Dactyloidae iOS initial commit
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
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
|
|
@ -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
|
|
@ -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))
|
||||
674
mobile/ios/Client/Assets/About/Licenses.html
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: sans-serif;
|
||||
src: url('/reader-mode/fonts/FiraSans-Regular.ttf');
|
||||
}
|
||||
body,p,h1,h2,h3 {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
h2 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.link {
|
||||
margin: 0;
|
||||
text-size: 55%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<h1>Licenses</h1>
|
||||
|
||||
<h2>Firefox for iOS</h2>
|
||||
<p class="link"><a href="https://github.com/mozilla/firefox-ios">github.com/mozilla/firefox-ios</a></p>
|
||||
<div class="text">
|
||||
|
||||
|
||||
|
||||
|
||||
<h3 id="mozilla-public-license-version-2.0">Mozilla Public License<br>Version 2.0</h3>
|
||||
<h4 id="definitions">1. Definitions</h4>
|
||||
<dl>
|
||||
<dt>1.1. “Contributor”</dt>
|
||||
<dd><p>means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software.</p>
|
||||
</dd>
|
||||
<dt>1.2. “Contributor Version”</dt>
|
||||
<dd><p>means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution.</p>
|
||||
</dd>
|
||||
<dt>1.3. “Contribution”</dt>
|
||||
<dd><p>means Covered Software of a particular Contributor.</p>
|
||||
</dd>
|
||||
<dt>1.4. “Covered Software”</dt>
|
||||
<dd><p>means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof.</p>
|
||||
</dd>
|
||||
<dt>1.5. “Incompatible With Secondary Licenses”</dt>
|
||||
<dd><p>means</p>
|
||||
<ol type="a">
|
||||
<li><p>that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or</p></li>
|
||||
<li><p>that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License.</p></li>
|
||||
</ol>
|
||||
</dd>
|
||||
<dt>1.6. “Executable Form”</dt>
|
||||
<dd><p>means any form of the work other than Source Code Form.</p>
|
||||
</dd>
|
||||
<dt>1.7. “Larger Work”</dt>
|
||||
<dd><p>means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software.</p>
|
||||
</dd>
|
||||
<dt>1.8. “License”</dt>
|
||||
<dd><p>means this document.</p>
|
||||
</dd>
|
||||
<dt>1.9. “Licensable”</dt>
|
||||
<dd><p>means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License.</p>
|
||||
</dd>
|
||||
<dt>1.10. “Modifications”</dt>
|
||||
<dd><p>means any of the following:</p>
|
||||
<ol type="a">
|
||||
<li><p>any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or</p></li>
|
||||
<li><p>any new file in Source Code Form that contains any Covered Software.</p></li>
|
||||
</ol>
|
||||
</dd>
|
||||
<dt>1.11. “Patent Claims” of a Contributor</dt>
|
||||
<dd><p>means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version.</p>
|
||||
</dd>
|
||||
<dt>1.12. “Secondary License”</dt>
|
||||
<dd><p>means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses.</p>
|
||||
</dd>
|
||||
<dt>1.13. “Source Code Form”</dt>
|
||||
<dd><p>means the form of the work preferred for making modifications.</p>
|
||||
</dd>
|
||||
<dt>1.14. “You” (or “Your”)</dt>
|
||||
<dd><p>means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
<h4 id="license-grants-and-conditions">2. License Grants and Conditions</h4>
|
||||
<h3 id="grants">2.1. Grants</h3>
|
||||
<p>Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:</p>
|
||||
<ol type="a">
|
||||
<li><p>under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and</p></li>
|
||||
<li><p>under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version.</p></li>
|
||||
</ol>
|
||||
<h3 id="effective-date">2.2. Effective Date</h3>
|
||||
<p>The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution.</p>
|
||||
<h3 id="limitations-on-grant-scope">2.3. Limitations on Grant Scope</h3>
|
||||
<p>The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor:</p>
|
||||
<ol type="a">
|
||||
<li><p>for any code that a Contributor has removed from Covered Software; or</p></li>
|
||||
<li><p>for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or</p></li>
|
||||
<li><p>under Patent Claims infringed by Covered Software in the absence of its Contributions.</p></li>
|
||||
</ol>
|
||||
<p>This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4).</p>
|
||||
<h3 id="subsequent-licenses">2.4. Subsequent Licenses</h3>
|
||||
<p>No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3).</p>
|
||||
<h3 id="representation">2.5. Representation</h3>
|
||||
<p>Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License.</p>
|
||||
<h3 id="fair-use">2.6. Fair Use</h3>
|
||||
<p>This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents.</p>
|
||||
<h3 id="conditions">2.7. Conditions</h3>
|
||||
<p>Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1.</p>
|
||||
<h4 id="responsibilities">3. Responsibilities</h4>
|
||||
<h3 id="distribution-of-source-form">3.1. Distribution of Source Form</h3>
|
||||
<p>All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form.</p>
|
||||
<h3 id="distribution-of-executable-form">3.2. Distribution of Executable Form</h3>
|
||||
<p>If You distribute Covered Software in Executable Form then:</p>
|
||||
<ol type="a">
|
||||
<li><p>such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and</p></li>
|
||||
<li><p>You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License.</p></li>
|
||||
</ol>
|
||||
<h3 id="distribution-of-a-larger-work">3.3. Distribution of a Larger Work</h3>
|
||||
<p>You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s).</p>
|
||||
<h3 id="notices">3.4. Notices</h3>
|
||||
<p>You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies.</p>
|
||||
<h3 id="application-of-additional-terms">3.5. Application of Additional Terms</h3>
|
||||
<p>You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction.</p>
|
||||
<h4 id="inability-to-comply-due-to-statute-or-regulation">4. Inability to Comply Due to Statute or Regulation</h4>
|
||||
<p>If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it.</p>
|
||||
<h4 id="termination">5. Termination</h4>
|
||||
<p>5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice.</p>
|
||||
<p>5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.</p>
|
||||
<p>5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination.</p>
|
||||
<h4 id="disclaimer-of-warranty">6. Disclaimer of Warranty</h4>
|
||||
<p><em>Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer.</em></p>
|
||||
<h4 id="limitation-of-liability">7. Limitation of Liability</h4>
|
||||
<p><em>Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You.</em></p>
|
||||
<h4 id="litigation">8. Litigation</h4>
|
||||
<p>Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims.</p>
|
||||
<h4 id="miscellaneous">9. Miscellaneous</h4>
|
||||
<p>This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor.</p>
|
||||
<h4 id="versions-of-the-license">10. Versions of the License</h4>
|
||||
<h3 id="new-versions">10.1. New Versions</h3>
|
||||
<p>Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number.</p>
|
||||
<h3 id="effect-of-new-versions">10.2. Effect of New Versions</h3>
|
||||
<p>You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward.</p>
|
||||
<h3 id="modified-versions">10.3. Modified Versions</h3>
|
||||
<p>If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License).</p>
|
||||
<h3 id="distributing-source-code-form-that-is-incompatible-with-secondary-licenses">10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses</h3>
|
||||
<p>If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached.</p>
|
||||
<h4 id="exhibit-a---source-code-form-license-notice">Exhibit A - Source Code Form License Notice</h4>
|
||||
<blockquote>
|
||||
<p>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/.</p>
|
||||
</blockquote>
|
||||
<p>If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice.</p>
|
||||
<p>You may add additional accurate notices of copyright ownership.</p>
|
||||
<h4 id="exhibit-b---incompatible-with-secondary-licenses-notice">Exhibit B - “Incompatible With Secondary Licenses” Notice</h4>
|
||||
<blockquote>
|
||||
<p>This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0.</p>
|
||||
</blockquote>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<center><p>-</p></center>
|
||||
|
||||
<!-- -->
|
||||
|
||||
<h2>Alamofire</h2>
|
||||
<p class="link"><a href="http://alamofire.org">alamofire.org</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2014 Alamofire (http://alamofire.org/)</p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
<!-- -->
|
||||
|
||||
<h2>Base32</h2>
|
||||
<p class="link"><a href="https://github.com/norio-nomura/Base32">github.com/norio-nomura/Base32</a></p>
|
||||
<div class="text">
|
||||
<p>The MIT License (MIT)</p>
|
||||
|
||||
<p>Copyright (c) 2015 Norio Nomura</p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
<h2>Box</h2>
|
||||
<p class="link"><a href="https://github.com/robrix/Box">github.com/robrix/Box</a></p>
|
||||
<div class="text">
|
||||
<p>The MIT License (MIT)</p>
|
||||
|
||||
<p>Copyright (c) 2014 Rob Rix</p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
<h2>Deferred</h2>
|
||||
<p class="link"><a href="https://github.com/bignerdranch/Deferred">github.com/bignerdranch/Deferred</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2014 John Gallagher <jgallagher@bignerdranch.com>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.</p>
|
||||
</div>
|
||||
|
||||
<h2>FilledPageControl</h2>
|
||||
<p>Copyright (c) 2016 Kyle Zaragoza <popwarsweet@gmail.com></p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.</p>
|
||||
|
||||
|
||||
<h2>GCDWebServer</h2>
|
||||
<p class="link"><a href="https://github.com/swisspol/GCDWebServer">github.com/swisspol/GCDWebServer</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2012-2014, Pierre-Olivier Latour</p>
|
||||
<p>All rights reserved.</p>
|
||||
|
||||
<p>Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:</p>
|
||||
|
||||
<ul>
|
||||
<li>Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.</li>
|
||||
<li>Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.</li>
|
||||
<li>The name of Pierre-Olivier Latour may not be used to endorse
|
||||
or promote products derived from this software without specific
|
||||
prior written permission.</li>
|
||||
</ul>
|
||||
|
||||
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>KIF</h2>
|
||||
<p class="link"><a href="https://github.com/kif-framework/KIF">https://github.com/kif-framework/KIF</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright 2011 Square, Inc.</p>
|
||||
<p>A full list of contributors is available at https://github.com/square/KIF/contributors</p>
|
||||
|
||||
<p>Licensed under the Apache License, Version 2.0 (the "License");</p>
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at</p>
|
||||
|
||||
<p>http://www.apache.org/licenses/LICENSE-2.0</p>
|
||||
|
||||
<p>Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.</p>
|
||||
</div>
|
||||
|
||||
<h2>Result</h2>
|
||||
<p class="link"><a href="https://github.com/bignerdranch/Result">github.com/bignerdranch/Result</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2014 John Gallagher <jgallagher@bignerdranch.com></p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.</p>
|
||||
</div>
|
||||
|
||||
<h2>SDWebImage</h2>
|
||||
<p class="link"><a href="https://github.com/rs/SDWebImage">github.com/rs/SDWebImage</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2009 Olivier Poitrey <rs@dailymotion.com></p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>SQLite.swift</h2>
|
||||
<p class="link"><a href="https://github.com/stephencelis/SQLite.swift">github.com/stephencelis/SQLite.swift</a></p>
|
||||
<div class="text">
|
||||
<p>(The MIT License)</p>
|
||||
|
||||
<p>Copyright (c) 2014-2015 Stephen Celis (<stephen@stephencelis.com>)</p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>Fuzi</h2>
|
||||
<p class="link"><a href="https://github.com/cezheng/Fuzi">github.com/cezheng/Fuzi</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2015 Ce Zheng</p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>Snap</h2>
|
||||
<p class="link"><a href="https://github.com/SnapKit">github.com/SnapKit</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit</p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>XCGLogger</h2>
|
||||
<p class="link"><a href="https://github.com/DaveWoodCom/XCGLogger">github.com/DaveWoodCom/XCGLogger</a></p>
|
||||
<div class="text">
|
||||
<p>The MIT License (MIT)</p>
|
||||
|
||||
<p>Copyright (c) 2014 Dave Wood, Cerebral Gardens http://www.cerebralgardens.com/</p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>Readability</h2>
|
||||
<p class="link"><a href="https://github.com/mozilla/readability">github.com/mozilla/readability</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2010 Arc90 Inc</p>
|
||||
|
||||
<p>Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at</p>
|
||||
|
||||
<p>http://www.apache.org/licenses/LICENSE-2.0</p>
|
||||
|
||||
<p>Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>sqlchipher</h2>
|
||||
<p class="link"><a href="https://github.com/sqlcipher/sqlcipher">github.com/sqlcipher/sqlcipher</a></p>
|
||||
<div class="text">
|
||||
<p>Copyright (c) 2008, ZETETIC LLC</p>
|
||||
<p>All rights reserved.</p>
|
||||
|
||||
<p>Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:</p>
|
||||
|
||||
<ul>
|
||||
<li>Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.</li>
|
||||
<li>Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.</li>
|
||||
<li>Neither the name of the ZETETIC LLC nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.</li>
|
||||
</ul>
|
||||
|
||||
<p>THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>SwiftKeychainWrapper</h2>
|
||||
<p class="link"><a href="https://github.com/jrendel/SwiftKeychainWrapper">github.com/jrendel/SwiftKeychainWrapper</a></p>
|
||||
<div class="text">
|
||||
<p>The MIT License (MIT)</p>
|
||||
|
||||
<p>Copyright (c) 2014 Jason</p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.</p>
|
||||
</div>
|
||||
<center><p>-</p></center>
|
||||
|
||||
|
||||
<h2>Swift-JSON</h2>
|
||||
<p class="link"><a href="https://github.com/dankogai/swift-json">https://github.com/dankogai/swift-json</a></p>
|
||||
<div class="text">
|
||||
<p>The MIT License (MIT)</p>
|
||||
|
||||
<p>Copyright (c) 2014 Dan Kogai</p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>UIImageColors</h2>
|
||||
<p class="link"><a href="https://github.com/jathu/UIImageColors">github.com/jathu/UIImageColors</a></p>
|
||||
|
||||
<p>Created by Jathu Satkunarajah (@jathu) on 2015-06-11 - Toronto</p>
|
||||
<p>Original Cocoa version by Panic Inc. - Portland</p>
|
||||
|
||||
<p>MIT License</p>
|
||||
|
||||
<p>Copyright (c) 2015 Jathu Satkunarajah</p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.</p>
|
||||
</div>
|
||||
|
||||
<h2>UIImageViewAligned</h2>
|
||||
<p class="link"><a href="https://github.com/reydanro/UIImageViewAligned">github.com/reydanro/UIImageViewAligned</a></p>
|
||||
<div class="text">
|
||||
<p>The MIT License (MIT)</p>
|
||||
|
||||
<p>Copyright (c) 2013 Andrei Stanescu</p>
|
||||
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
76
mobile/ios/Client/Assets/CertError.css
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/* 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/. */
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: white;
|
||||
padding: 0 65px;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
font-family: "Helvetica Neue Medium" sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-family: "Helvetica Neue Medium" sans-serif;
|
||||
font-weight: 300;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
/* Add a set of stripes at the top of pages */
|
||||
#decoration {
|
||||
background-image: repeating-linear-gradient(-65deg, #ed2, #ed2 10px,
|
||||
#fe3 10px, #fe3 20px,
|
||||
#ed2 20px);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(76, 158, 255);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
font-family: "Helvetica Neue Medium" sans-serif;
|
||||
background-color: rgb(76, 158, 255);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#errorPageContainer {
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
-webkit-transform: translateY(70px);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
#certErrorCode {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#advancedButton {
|
||||
background-color: white;
|
||||
color: #777;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
|
||||
#advancedContent {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
49
mobile/ios/Client/Assets/CertError.html
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<!-- 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/. -->
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>%error_title%</title>
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<link rel="stylesheet" href="CertError.css" type="text/css" media="all" />
|
||||
|
||||
<script type="application/javascript">
|
||||
// If the user swipes back and forth to this tab, we want to make sure we make an attempt to reload the original page.
|
||||
var fresh = true;
|
||||
window.addEventListener('popstate', function () {
|
||||
if (fresh)
|
||||
fresh = false;
|
||||
else
|
||||
webkit.messageHandlers.localRequestHelper.postMessage({ type: "reload" });
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="errorPageContainer">
|
||||
<h1 id="errorTitleText">%error_title%</h1>
|
||||
<p id="actions">%actions%</p>
|
||||
<p>%long_description%</p>
|
||||
<button id="advancedButton">%advanced_button%</button>
|
||||
|
||||
<div id="advancedContent">
|
||||
<p id="certErrorCode">%cert_error%</p>
|
||||
<p>%warning_advanced1%</p>
|
||||
<p>%warning_advanced2%</p>
|
||||
<div>%warning_actions%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="decoration"></div>
|
||||
|
||||
<script>
|
||||
var advancedContent = document.getElementById("advancedContent");
|
||||
advancedContent.style.display = "none";
|
||||
document.getElementById("advancedButton").onclick = function () {
|
||||
this.style.display = "none";
|
||||
advancedContent.style.display = "block";
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
28
mobile/ios/Client/Assets/ContextMenu.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* 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/. */
|
||||
|
||||
window.addEventListener('touchstart', function(evt) {
|
||||
var target = evt.target;
|
||||
|
||||
var targetLink = target.closest('a');
|
||||
var targetImage = target.closest('img');
|
||||
|
||||
if (!targetLink && !targetImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {};
|
||||
|
||||
if (targetLink) {
|
||||
data.link = targetLink.href;
|
||||
}
|
||||
|
||||
if (targetImage) {
|
||||
data.image = targetImage.src;
|
||||
}
|
||||
|
||||
if (data.link || data.image) {
|
||||
webkit.messageHandlers.contextMenuMessageHandler.postMessage(data);
|
||||
}
|
||||
}, true);
|
||||
42
mobile/ios/Client/Assets/CustomSearchHelper.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
if (!window.__firefox__) {
|
||||
Object.defineProperty(window, '__firefox__', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: {}
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(window.__firefox__, 'searchQueryForField', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: function() {
|
||||
var input = document.activeElement;
|
||||
if (input.tagName.toLowerCase() !== 'input') return null;
|
||||
var form = input.form;
|
||||
if (!form || form.method.toLowerCase() != 'get') return null;
|
||||
|
||||
var inputs = form.getElementsByTagName('input');
|
||||
inputs = Array.prototype.slice.call(inputs, 0);
|
||||
var params = inputs.map(function(element) {
|
||||
if (element.name == input.name) return [element.name, '{searchTerms}'].join('=');
|
||||
return [element.name, element.value].map(encodeURIComponent).join('=');
|
||||
});
|
||||
|
||||
var selectFields = form.getElementsByTagName('select');
|
||||
selectFields = Array.prototype.slice.call(selectFields, 0);
|
||||
var selectParams = selectFields.map(function(e){
|
||||
return [e.name, e.options[e.selectedIndex].value].map(encodeURIComponent).join('=');
|
||||
});
|
||||
params = params.concat(selectParams);
|
||||
if (!form.action) return null; //an invalid form.
|
||||
var url = [form.action, params.join('&')].join('?');
|
||||
return url;
|
||||
}
|
||||
});
|
||||
60
mobile/ios/Client/Assets/Favicons.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* 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/. */
|
||||
|
||||
|
||||
if (!window.__firefox__) {
|
||||
Object.defineProperty(window, '__firefox__', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: {}
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(window.__firefox__, 'favicons', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: (function() {
|
||||
// These integers should be kept in sync with the IconType raw-values
|
||||
var ICON = 0;
|
||||
var APPLE = 1;
|
||||
var APPLE_PRECOMPOSED = 2;
|
||||
var GUESS = 3;
|
||||
|
||||
var selectors = {
|
||||
"link[rel~='icon']": ICON,
|
||||
"link[rel='apple-touch-icon']": APPLE,
|
||||
"link[rel='apple-touch-icon-precomposed']": APPLE_PRECOMPOSED
|
||||
};
|
||||
|
||||
function getAll() {
|
||||
var favicons = {};
|
||||
|
||||
for (var selector in selectors) {
|
||||
var icons = document.querySelectorAll(selector);
|
||||
for (var i = 0; i < icons.length; i++) {
|
||||
var href = icons[i].href;
|
||||
favicons[href] = selectors[selector];
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find anything in the page, look to see if a favicon.ico file exists for the domain
|
||||
if (Object.keys(favicons).length === 0) {
|
||||
var href = document.location.origin + "/favicon.ico";
|
||||
favicons[href] = GUESS;
|
||||
}
|
||||
return favicons;
|
||||
}
|
||||
|
||||
function getFavicons() {
|
||||
var favicons = getAll();
|
||||
webkit.messageHandlers.faviconsMessageHandler.postMessage(favicons);
|
||||
}
|
||||
|
||||
return {
|
||||
getFavicons: getFavicons
|
||||
};
|
||||
})()
|
||||
});
|
||||
292
mobile/ios/Client/Assets/FindInPage.js
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
/* 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/. */
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
var DEBUG_ENABLED = false;
|
||||
var MATCH_HIGHLIGHT_ACTIVE = "#f19750";
|
||||
var MATCH_HIGHLIGHT_INACTIVE = "#ffde49";
|
||||
var SCROLL_INTERVAL_INCREMENT = 5;
|
||||
var SCROLL_INTERVAL_DURATION = 400;
|
||||
var SCROLL_OFFSET = 60;
|
||||
|
||||
var activeHighlightSpan = null;
|
||||
var lastSearch;
|
||||
var scrollInterval;
|
||||
var activeIndex = 0;
|
||||
var highlightSpans = [];
|
||||
|
||||
function debug(str) {
|
||||
if (DEBUG_ENABLED) {
|
||||
console.log("FindInPage: " + str);
|
||||
}
|
||||
}
|
||||
|
||||
function isElementVisible(elem) {
|
||||
return getComputedStyle(elem).visibility !== "hidden";
|
||||
}
|
||||
|
||||
function isRectInViewport(rect) {
|
||||
var left = rect.left + document.body.scrollLeft;
|
||||
var right = rect.right + document.body.scrollLeft;
|
||||
var top = rect.top + document.body.scrollTop;
|
||||
var bottom = rect.bottom + document.body.scrollTop;
|
||||
|
||||
return rect.width > 0 &&
|
||||
rect.height > 0 &&
|
||||
right >= 0 &&
|
||||
bottom >= 0 &&
|
||||
left <= document.body.scrollWidth &&
|
||||
top <= document.body.scrollHeight;
|
||||
}
|
||||
|
||||
function findMatches(text) {
|
||||
// For case-insensitive matching.
|
||||
var lowerText = text.toLocaleLowerCase();
|
||||
var upperText = text.toLocaleUpperCase();
|
||||
|
||||
var matches = [];
|
||||
var range = document.createRange();
|
||||
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
|
||||
var textLength = text.length;
|
||||
var node;
|
||||
while (node = walker.nextNode()) {
|
||||
var textContent = node.textContent;
|
||||
findString: for (var i = 0; i < textContent.length - textLength + 1; ++i) {
|
||||
for (var j = 0; j < textLength; ++j) {
|
||||
var nextChar = textContent[i + j];
|
||||
if (lowerText[j] !== nextChar && upperText[j] !== nextChar) {
|
||||
continue findString;
|
||||
}
|
||||
}
|
||||
|
||||
// This node is a TextNode, not an Element. Its parent is the nearest Element.
|
||||
var element = node.parentNode;
|
||||
|
||||
// Find the rect of just the text for this match.
|
||||
range.setStart(node, i);
|
||||
range.setEnd(node, i + textLength);
|
||||
var textRect = range.getBoundingClientRect();
|
||||
|
||||
// We have a match, but we need to make sure it's visible. The condition
|
||||
// below checks the following cases:
|
||||
// * If this element or any of its parents has style visibility hidden.
|
||||
// The visibility style is inherited, so we need to check only this
|
||||
// element and not all of its ancestors.
|
||||
// * If the highlight will be outside of the page's bounds. We determine
|
||||
// this by comparing the bounds of the text rect.
|
||||
// * If the element style display is set to none. display:none collapses
|
||||
// the element's space, so this will again be detected by looking at
|
||||
// the text's rect: if the element is collapsed, the width and height
|
||||
// will be zero.
|
||||
if (isElementVisible(element) && isRectInViewport(textRect)) {
|
||||
matches.push({ node: node, index: i });
|
||||
|
||||
// Resume searching after this match to prevent overlapping results.
|
||||
i += textLength- 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function flattenNode(node) {
|
||||
var parent = node.parentNode;
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (node.firstChild) {
|
||||
parent.insertBefore(node.firstChild, node);
|
||||
}
|
||||
|
||||
node.remove();
|
||||
parent.normalize();
|
||||
}
|
||||
|
||||
function clearHighlights() {
|
||||
if (highlightSpans.length > 0) {
|
||||
for (var span of highlightSpans) {
|
||||
flattenNode(span);
|
||||
}
|
||||
highlightSpans = [];
|
||||
}
|
||||
|
||||
activeHighlightSpan = null;
|
||||
}
|
||||
|
||||
function highlightAllMatches(text) {
|
||||
debug("Searching: " + text);
|
||||
|
||||
clearHighlights();
|
||||
|
||||
if (!text.trim()) {
|
||||
webkit.messageHandlers.findInPageHandler.postMessage({ totalResults: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
var range = document.createRange();
|
||||
var matches = findMatches(text);
|
||||
var highlightTemplate = document.createElement("span");
|
||||
highlightTemplate.style.backgroundColor = MATCH_HIGHLIGHT_INACTIVE;
|
||||
|
||||
// If there are multiple matches in the same node, inserting a highlight span before other matches
|
||||
// in that node will invalidate other matches since the node itself changes. By iterating through
|
||||
// results in reverse, we highlight matches last in the node first so earlier matches are unaffected.
|
||||
for (var i = matches.length - 1; i >= 0; --i) {
|
||||
var match = matches[i];
|
||||
var highlight = highlightTemplate.cloneNode();
|
||||
|
||||
range.setStart(match.node, match.index);
|
||||
range.setEnd(match.node, match.index + text.length);
|
||||
range.surroundContents(highlight);
|
||||
highlightSpans.unshift(highlight);
|
||||
}
|
||||
|
||||
debug(matches.length + " highlighted rects created!");
|
||||
webkit.messageHandlers.findInPageHandler.postMessage({ totalResults: matches.length });
|
||||
}
|
||||
|
||||
function getIDForRect(rect) {
|
||||
return rect.top + "," + rect.bottom + "," + rect.left + "," + rect.right;
|
||||
}
|
||||
|
||||
function updateActiveHighlight() {
|
||||
// Reset the color of the previous highlight.
|
||||
if (activeHighlightSpan) {
|
||||
activeHighlightSpan.style.backgroundColor = MATCH_HIGHLIGHT_INACTIVE;
|
||||
}
|
||||
|
||||
if (!highlightSpans.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeHighlightSpan = highlightSpans[activeIndex];
|
||||
activeHighlightSpan.style.backgroundColor = MATCH_HIGHLIGHT_ACTIVE;
|
||||
|
||||
// Find the position of the element centered on the screen, then scroll to it.
|
||||
var rect = activeHighlightSpan.getBoundingClientRect();
|
||||
var top = SCROLL_OFFSET + rect.top + scrollY - window.innerHeight / 2;
|
||||
var left = rect.left + scrollX - window.innerWidth / 2;
|
||||
left = clamp(left, 0, document.body.scrollWidth);
|
||||
top = clamp(top, 0, document.body.scrollHeight);
|
||||
scrollToSelection(left, top, SCROLL_INTERVAL_DURATION);
|
||||
debug("Scrolled to: " + left + ", " + top);
|
||||
}
|
||||
|
||||
function scrollToSelection(left, top, duration) {
|
||||
var time = 0;
|
||||
var startX = scrollX;
|
||||
var startY = scrollY;
|
||||
clearInterval(scrollInterval);
|
||||
scrollInterval = setInterval(function() {
|
||||
var xStep = easeOutCubic(time, startX, left - startX, duration);
|
||||
var yStep = easeOutCubic(time, startY, top - startY, duration);
|
||||
window.scrollTo(xStep, yStep);
|
||||
time += SCROLL_INTERVAL_INCREMENT;
|
||||
if (time >= duration) {
|
||||
clearInterval(scrollInterval);
|
||||
}
|
||||
}, SCROLL_INTERVAL_INCREMENT);
|
||||
}
|
||||
|
||||
function easeOutCubic(currentTime, startValue, changeInValue, duration) {
|
||||
return changeInValue * (Math.pow(currentTime / duration - 1, 3) + 1) + startValue;
|
||||
}
|
||||
|
||||
function clamp(number, min, max) {
|
||||
return Math.max(min, Math.min(number, max));
|
||||
}
|
||||
|
||||
function updateSearch(text) {
|
||||
if (lastSearch == text) {
|
||||
// The text is the same, so we're either finding either the next or previous result.
|
||||
var totalResults = highlightSpans.length;
|
||||
activeIndex = (activeIndex + totalResults) % totalResults;
|
||||
} else {
|
||||
// Store the current active rect to decide which new match should be active.
|
||||
var activeHighlightRect = null;
|
||||
if (activeHighlightSpan) {
|
||||
activeHighlightRect = activeHighlightSpan.getBoundingClientRect();
|
||||
}
|
||||
|
||||
// The search text changed, so scan the page for new results.
|
||||
highlightAllMatches(text);
|
||||
|
||||
// If we found a match at or after the last match, use that position
|
||||
// instead of starting again from the top.
|
||||
activeIndex = 0;
|
||||
if (activeHighlightRect) {
|
||||
for (var i = 0; i < highlightSpans.length; i++) {
|
||||
var highlight = highlightSpans[i];
|
||||
var highlightRect = highlight.getBoundingClientRect();
|
||||
if ((highlightRect.top == activeHighlightRect.top && highlightRect.left >= activeHighlightRect.left) ||
|
||||
(highlightRect.top > activeHighlightRect.top)) {
|
||||
activeIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastSearch = text;
|
||||
}
|
||||
|
||||
// Update the UI with the current match index.
|
||||
var currentResult = highlightSpans.length ? activeIndex + 1 : 0;
|
||||
webkit.messageHandlers.findInPageHandler.postMessage({ currentResult: currentResult });
|
||||
|
||||
updateActiveHighlight();
|
||||
}
|
||||
|
||||
if (!window.__firefox__) {
|
||||
Object.defineProperty(window, '__firefox__', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: {}
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(window.__firefox__, 'find', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: function(text) {
|
||||
updateSearch(text);
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(window.__firefox__, 'findNext', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: function(text) {
|
||||
activeIndex++;
|
||||
updateSearch(text);
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(window.__firefox__, 'findPrevious', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: function(text) {
|
||||
activeIndex--;
|
||||
updateSearch(text);
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(window.__firefox__, 'findDone', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: function() {
|
||||
clearHighlights();
|
||||
lastSearch = null;
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
BIN
mobile/ios/Client/Assets/Fonts/CharisSILB.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/CharisSILBI.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/CharisSILI.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/CharisSILR.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-Bold.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-BoldItalic.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-Book.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-Italic.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-Light.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-Medium.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-Regular.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-SemiBold.ttf
Normal file
BIN
mobile/ios/Client/Assets/Fonts/FiraSans-UltraLight.ttf
Normal file
15
mobile/ios/Client/Assets/FxASignIn.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Transport postMessage events from the fxa-content-server to the embedding
|
||||
* webview.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
function handleAccountsCommand(evt) {
|
||||
webkit.messageHandlers.accountsCommandHandler.postMessage({ type: evt.type, detail: evt.detail });
|
||||
};
|
||||
window.addEventListener("FirefoxAccountsCommand", handleAccountsCommand);
|
||||
36
mobile/ios/Client/Assets/HistoryStateHelper.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/* 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/. */
|
||||
|
||||
(function() {
|
||||
var nativeHistoryPushState = window.history.pushState;
|
||||
var nativeHistoryReplaceState = window.history.replaceState;
|
||||
|
||||
// We need to catch calls to `history.pushState()` in order to
|
||||
// notify the BrowserViewController so that history can be
|
||||
// recorded in single-page web applications that do all rendering
|
||||
// on the client side.
|
||||
window.history.pushState = function(state, title, url) {
|
||||
nativeHistoryPushState.apply(this, arguments);
|
||||
webkit.messageHandlers.historyStateHelper.postMessage({
|
||||
pushState: true,
|
||||
state: state,
|
||||
title: title,
|
||||
url: url
|
||||
});
|
||||
};
|
||||
|
||||
// We need to catch calls to `history.replaceState()` in order to
|
||||
// notify the BrowserViewController so that history can be
|
||||
// recorded in single-page web applications that do all rendering
|
||||
// on the client side.
|
||||
window.history.replaceState = function(state, title, url) {
|
||||
nativeHistoryReplaceState.apply(this, arguments);
|
||||
webkit.messageHandlers.historyStateHelper.postMessage({
|
||||
replaceState: true,
|
||||
state: state,
|
||||
title: title,
|
||||
url: url
|
||||
});
|
||||
};
|
||||
})();
|
||||
BIN
mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch.png
vendored
Normal file
|
After Width: | Height: | Size: 449 B |
BIN
mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 831 B |
BIN
mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
23
mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "AddSearch.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "AddSearch@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "AddSearch@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-40.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-60.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-29.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "57x57",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "57x57",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-29.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-40.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "50x50",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "50x50",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "72x72",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "72x72",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "icon-512@2x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -0,0 +1,151 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-40.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-60.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-29.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "57x57",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "57x57",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon-60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-40.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-29.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-40.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "50x50",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "50x50",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "72x72",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "72x72",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon-83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "icon-512@2x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,149 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-60@2x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "57x57",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "57x57",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small-40.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "50x50",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small-50.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "50x50",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small-50@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "72x72",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-72.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "72x72",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-72@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-iPadPro@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 8 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
6
mobile/ios/Client/Assets/Images.xcassets/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
23
mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "close.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "close@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "close@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/close.png
vendored
Normal file
|
After Width: | Height: | Size: 574 B |
BIN
mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/close@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 898 B |
BIN
mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/close@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
23
mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "down-caret.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "down-caret@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "down-caret@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/down-caret.png
vendored
Normal file
|
After Width: | Height: | Size: 293 B |
BIN
mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/down-caret@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 463 B |
BIN
mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/down-caret@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 674 B |
23
mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "up-caret.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "up-caret@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "up-caret@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/up-caret.png
vendored
Normal file
|
After Width: | Height: | Size: 301 B |
BIN
mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/up-caret@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 449 B |
BIN
mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/up-caret@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 666 B |
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||