diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a65778a..f8b6d9db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,73 @@ +Changelog for ownCloud iOS Client [11.9.0] (2022-03-16) +======================================= +The following sections list the changes in ownCloud iOS Client 11.9.0 relevant to +ownCloud admins and users. + +[11.9.0]: https://github.com/owncloud/ios-app/compare/milestone/11.8.2...milestone/11.9.0 + +Summary +------- + +* Bugfix - Fix WebDAV endpoint URL for media playback after restoration: [#1093](https://github.com/owncloud/ios-app/pull/1093) +* Bugfix - OAuth token renewal race condition: [#1105](https://github.com/owncloud/ios-app/pull/1105) +* Change - Biometrical Authentication Button: [#1004](https://github.com/owncloud/ios-app/issues/1004) +* Change - Poll for changes efficiency enhancements: [#1043](https://github.com/owncloud/ios-app/pull/1043) +* Change - Webfinger / server location: [#1059](https://github.com/owncloud/ios-app/pull/1059) +* Change - Infinite PROPFIND support: [#950](https://github.com/owncloud/ios-app/issues/950) +* Change - Rename Account (without re-authentication): [#972](https://github.com/owncloud/ios-app/issues/972) + +Details +------- + +* Bugfix - Fix WebDAV endpoint URL for media playback after restoration: [#1093](https://github.com/owncloud/ios-app/pull/1093) + + Fixes a bug where media playback failed with a 404 Not Found error after restoration because the + WebDAV endpoint URL was constructed from authentication data rather than OC user endpoint + data. + + https://github.com/owncloud/ios-app/pull/1093 + +* Bugfix - OAuth token renewal race condition: [#1105](https://github.com/owncloud/ios-app/pull/1105) + + Retry requests that failed with a 401 during a token refresh + + https://github.com/owncloud/ios-app/pull/1105 + +* Change - Biometrical Authentication Button: [#1004](https://github.com/owncloud/ios-app/issues/1004) + + Added biometrical authentication button to provide a fallback for the fileprovider or app, if + the automatically biometrical unlock does not work, or the user cancel the biometrical + authentication flow. + + https://github.com/owncloud/ios-app/issues/1004 + +* Change - Poll for changes efficiency enhancements: [#1043](https://github.com/owncloud/ios-app/pull/1043) + + Avoids simultaneous polling for changes by FileProvider and app. + + https://github.com/owncloud/ios-app/pull/1043 + +* Change - Webfinger / server location: [#1059](https://github.com/owncloud/ios-app/pull/1059) + + Allows using webfinger or a lookup table to locate and use an alternative server based on the + user name + + https://github.com/owncloud/ios-app/pull/1059 + +* Change - Infinite PROPFIND support: [#950](https://github.com/owncloud/ios-app/issues/950) + + Added support for prepopulation of newly created account bookmarks via infinite PROPFINDs, + which speeds up the initial scan + + https://github.com/owncloud/ios-app/issues/950 + +* Change - Rename Account (without re-authentication): [#972](https://github.com/owncloud/ios-app/issues/972) + + Check if only the account name was changed in edit mode: save and dismiss without + re-authentication + + https://github.com/owncloud/ios-app/issues/972 + Changelog for ownCloud iOS Client [11.8.2] (2022-01-17) ======================================= The following sections list the changes in ownCloud iOS Client 11.8.2 relevant to diff --git a/changelog/11.9.0_2022-03-16/1004 b/changelog/11.9.0_2022-03-16/1004 new file mode 100644 index 000000000..8d3d5e7eb --- /dev/null +++ b/changelog/11.9.0_2022-03-16/1004 @@ -0,0 +1,5 @@ +Change: Biometrical Authentication Button + +Added biometrical authentication button to provide a fallback for the fileprovider or app, if the automatically biometrical unlock does not work, or the user cancel the biometrical authentication flow. + +https://github.com/owncloud/ios-app/issues/1004 diff --git a/changelog/11.9.0_2022-03-16/1043 b/changelog/11.9.0_2022-03-16/1043 new file mode 100644 index 000000000..448636ab8 --- /dev/null +++ b/changelog/11.9.0_2022-03-16/1043 @@ -0,0 +1,5 @@ +Change: Poll for changes efficiency enhancements + +Avoids simultaneous polling for changes by FileProvider and app. + +https://github.com/owncloud/ios-app/pull/1043 diff --git a/changelog/11.9.0_2022-03-16/1059 b/changelog/11.9.0_2022-03-16/1059 new file mode 100644 index 000000000..d9a360d0b --- /dev/null +++ b/changelog/11.9.0_2022-03-16/1059 @@ -0,0 +1,5 @@ +Change: Webfinger / server location + +Allows using webfinger or a lookup table to locate and use an alternative server based on the user name + +https://github.com/owncloud/ios-app/pull/1059 diff --git a/changelog/11.9.0_2022-03-16/1093 b/changelog/11.9.0_2022-03-16/1093 new file mode 100644 index 000000000..4f25dff08 --- /dev/null +++ b/changelog/11.9.0_2022-03-16/1093 @@ -0,0 +1,5 @@ +Bugfix: Fix WebDAV endpoint URL for media playback after restoration + +Fixes a bug where media playback failed with a 404 Not Found error after restoration because the WebDAV endpoint URL was constructed from authentication data rather than OC user endpoint data. + +https://github.com/owncloud/ios-app/pull/1093 diff --git a/changelog/11.9.0_2022-03-16/1105 b/changelog/11.9.0_2022-03-16/1105 new file mode 100644 index 000000000..f2fc16e70 --- /dev/null +++ b/changelog/11.9.0_2022-03-16/1105 @@ -0,0 +1,5 @@ +Bugfix: OAuth token renewal race condition + +Retry requests that failed with a 401 during a token refresh + +https://github.com/owncloud/ios-app/pull/1105 diff --git a/changelog/11.9.0_2022-03-16/950 b/changelog/11.9.0_2022-03-16/950 new file mode 100644 index 000000000..9273a01e0 --- /dev/null +++ b/changelog/11.9.0_2022-03-16/950 @@ -0,0 +1,5 @@ +Change: Infinite PROPFIND support + +Added support for prepopulation of newly created account bookmarks via infinite PROPFINDs, which speeds up the initial scan + +https://github.com/owncloud/ios-app/issues/950 diff --git a/changelog/11.9.0_2022-03-16/972 b/changelog/11.9.0_2022-03-16/972 new file mode 100644 index 000000000..0198a5e32 --- /dev/null +++ b/changelog/11.9.0_2022-03-16/972 @@ -0,0 +1,5 @@ +Change: Rename Account (without re-authentication) + +Check if only the account name was changed in edit mode: save and dismiss without re-authentication + +https://github.com/owncloud/ios-app/issues/972 diff --git a/doc/CONFIGURATION.json b/doc/CONFIGURATION.json index a84949d7d..631d8df97 100644 --- a/doc/CONFIGURATION.json +++ b/doc/CONFIGURATION.json @@ -527,6 +527,33 @@ "status" : "supported", "type" : "string" }, + { + "autoExpansion" : "none", + "category" : "Bookmarks", + "categoryTag" : "bookmarks", + "classIdentifier" : "bookmark", + "className" : "ownCloud.BookmarkViewController", + "description" : "Controls prepopulation of the local database with the full item set during account setup.", + "flatIdentifier" : "bookmark.prepopulation", + "key" : "prepopulation", + "label" : "bookmark.prepopulation", + "possibleValues" : [ + { + "description" : "No prepopulation. Request the contents of every folder individually.", + "value" : "doNot" + }, + { + "description" : "Parse the prepopulation metadata after receiving it as a whole.", + "value" : "split" + }, + { + "description" : "Parse the prepopulation metadata while receiving it.", + "value" : "streaming" + } + ], + "status" : "supported", + "type" : "string" + }, { "autoExpansion" : "none", "category" : "Bookmarks", diff --git a/docs/modules/ROOT/pages/ios_mdm_tables.adoc b/docs/modules/ROOT/pages/ios_mdm_tables.adoc index e1f49cf44..eccdb58df 100644 --- a/docs/modules/ROOT/pages/ios_mdm_tables.adoc +++ b/docs/modules/ROOT/pages/ios_mdm_tables.adoc @@ -314,6 +314,27 @@ tag::bookmarks[] |The default URL for the creation of new bookmarks. |supported `candidate` +|bookmark.prepopulation +|string +| +|Controls prepopulation of the local database with the full item set during account setup. +[cols="1,1"] +!=== +! Value +! Description +! `doNot` +! No prepopulation. Request the contents of every folder individually. + +! `split` +! Parse the prepopulation metadata after receiving it as a whole. + +! `streaming` +! Parse the prepopulation metadata while receiving it. + +!=== + +|supported `candidate` + |bookmark.url-editable |bool |`true` diff --git a/fastlane/metadata-emm/en-US/release_notes.txt b/fastlane/metadata-emm/en-US/release_notes.txt index f4c421176..353457a9c 100644 --- a/fastlane/metadata-emm/en-US/release_notes.txt +++ b/fastlane/metadata-emm/en-US/release_notes.txt @@ -1,6 +1,12 @@ -• Fix: PDF Editing -Fixed bug that prevents changes to PDFs being saved in place. +• Biometrical Authentication Button +Added a biometrical authentication button to the passcode unlock view. The biomatrical cancel button enables passcode unlock as fallback. -• Fix: Continuous Audio Playback -Fixed continuous audio playback, which stopped after two audio files. +• Rename Account +Renaming an account does no longer need a re-authentication + +• Media Playback +Fixes a bug where media playback failed + +• Faster Account Scan +We improved the time for the initial scan after setting up a new account diff --git a/fastlane/metadata-owncloud-online/en-US/release_notes.txt b/fastlane/metadata-owncloud-online/en-US/release_notes.txt index f4c421176..353457a9c 100644 --- a/fastlane/metadata-owncloud-online/en-US/release_notes.txt +++ b/fastlane/metadata-owncloud-online/en-US/release_notes.txt @@ -1,6 +1,12 @@ -• Fix: PDF Editing -Fixed bug that prevents changes to PDFs being saved in place. +• Biometrical Authentication Button +Added a biometrical authentication button to the passcode unlock view. The biomatrical cancel button enables passcode unlock as fallback. -• Fix: Continuous Audio Playback -Fixed continuous audio playback, which stopped after two audio files. +• Rename Account +Renaming an account does no longer need a re-authentication + +• Media Playback +Fixes a bug where media playback failed + +• Faster Account Scan +We improved the time for the initial scan after setting up a new account diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index f4c421176..353457a9c 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,6 +1,12 @@ -• Fix: PDF Editing -Fixed bug that prevents changes to PDFs being saved in place. +• Biometrical Authentication Button +Added a biometrical authentication button to the passcode unlock view. The biomatrical cancel button enables passcode unlock as fallback. -• Fix: Continuous Audio Playback -Fixed continuous audio playback, which stopped after two audio files. +• Rename Account +Renaming an account does no longer need a re-authentication + +• Media Playback +Fixes a bug where media playback failed + +• Faster Account Scan +We improved the time for the initial scan after setting up a new account diff --git a/ios-sdk b/ios-sdk index ef2a2d263..cd86774c3 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit ef2a2d2635d5fa58a307b82884f8b9e02cef97a1 +Subproject commit cd86774c3fa58492c910ba976e1251969583a780 diff --git a/ownCloud File Provider UI/DocumentActionViewController.swift b/ownCloud File Provider UI/DocumentActionViewController.swift index 758218b71..a7d0d6cb5 100644 --- a/ownCloud File Provider UI/DocumentActionViewController.swift +++ b/ownCloud File Provider UI/DocumentActionViewController.swift @@ -153,6 +153,7 @@ class DocumentActionViewController: FPUIActionExtensionViewController { override func prepare(forError error: Error) { if AppLockManager.supportedOnDevice { AppLockManager.shared.passwordViewHostViewController = self + AppLockManager.shared.biometricCancelLabel = "Cancel".localized AppLockManager.shared.cancelAction = { [weak self] in self?.complete(cancelWith: NSError(domain: FPUIErrorDomain, code: Int(FPUIExtensionErrorCode.userCancelled.rawValue), userInfo: nil)) } diff --git a/ownCloud File Provider/FileProviderExtension.m b/ownCloud File Provider/FileProviderExtension.m index 95d1dd7a0..269bf2b8a 100644 --- a/ownCloud File Provider/FileProviderExtension.m +++ b/ownCloud File Provider/FileProviderExtension.m @@ -189,17 +189,27 @@ - (NSFileProviderItem)itemForIdentifier:(NSFileProviderItemIdentifier)identifier if ([identifier isEqual:NSFileProviderRootContainerItemIdentifier]) { // Root item - [self.core.vault.database retrieveCacheItemsAtPath:@"/" itemOnly:YES completionHandler:^(OCDatabase *db, NSError *error, OCSyncAnchor syncAnchor, NSArray *items) { - item = items.firstObject; - returnError = error; + OCDatabase *database; + + if (((database = core.vault.database) != nil) && database.isOpened) + { + [database retrieveCacheItemsAtPath:@"/" itemOnly:YES completionHandler:^(OCDatabase *db, NSError *error, OCSyncAnchor syncAnchor, NSArray *items) { + item = items.firstObject; + returnError = error; + OCSyncExecDone(itemRetrieval); + }]; + } + else + { + // Database not available OCSyncExecDone(itemRetrieval); - }]; + } } else { // Other item - [self.core retrieveItemFromDatabaseForLocalID:(OCLocalID)identifier completionHandler:^(NSError *error, OCSyncAnchor syncAnchor, OCItem *itemFromDatabase) { + [core retrieveItemFromDatabaseForLocalID:(OCLocalID)identifier completionHandler:^(NSError *error, OCSyncAnchor syncAnchor, OCItem *itemFromDatabase) { item = itemFromDatabase; returnError = error; diff --git a/ownCloud Share Extension/ShareViewController.swift b/ownCloud Share Extension/ShareViewController.swift index ed95c98c4..7c6ce31fa 100644 --- a/ownCloud Share Extension/ShareViewController.swift +++ b/ownCloud Share Extension/ShareViewController.swift @@ -200,7 +200,7 @@ class ShareViewController: MoreStaticTableViewController { OnMainThread { self.navigationController?.popToViewController(self, animated: false) - let progressViewController = ProgressIndicatorViewController(initialProgressLabel: "Preparing…".localized, cancelHandler: {}) + let progressViewController = ProgressIndicatorViewController(initialProgressLabel: "Preparing…".localized, progress: nil, cancelHandler: {}) self.present(progressViewController, animated: false) diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 1dc80d25a..a92610413 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -4730,8 +4730,8 @@ APP_BUILD_FLAGS = "$(inherited)"; APP_BUILD_FLAGS_SWIFT = "$(APP_BUILD_FLAGS)"; APP_PRODUCT_NAME = ownCloud; - APP_SHORT_VERSION = 11.8.2; - APP_VERSION = 208; + APP_SHORT_VERSION = 11.9.0; + APP_VERSION = 211; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -4799,8 +4799,8 @@ APP_BUILD_FLAGS = "$(inherited)"; APP_BUILD_FLAGS_SWIFT = "$(APP_BUILD_FLAGS)"; APP_PRODUCT_NAME = ownCloud; - APP_SHORT_VERSION = 11.8.2; - APP_VERSION = 208; + APP_SHORT_VERSION = 11.9.0; + APP_VERSION = 211; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -4863,8 +4863,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - APP_SHORT_VERSION = 11.8.2; - APP_VERSION = 208; + APP_SHORT_VERSION = 11.9.0; + APP_VERSION = 211; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = ownCloud/ownCloud.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; @@ -4897,8 +4897,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - APP_SHORT_VERSION = 11.8.2; - APP_VERSION = 208; + APP_SHORT_VERSION = 11.9.0; + APP_VERSION = 211; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = ownCloud/ownCloud.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; @@ -5255,8 +5255,8 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; - APP_SHORT_VERSION = 11.8.2; - APP_VERSION = 208; + APP_SHORT_VERSION = 11.9.0; + APP_VERSION = 211; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = NO; @@ -5299,8 +5299,8 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; - APP_SHORT_VERSION = 11.8.2; - APP_VERSION = 208; + APP_SHORT_VERSION = 11.9.0; + APP_VERSION = 211; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = NO; diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File Provider.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File Provider.xcscheme index f96d48bbc..fa1aadec9 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File Provider.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File Provider.xcscheme @@ -152,18 +152,6 @@ isEnabled = "NO"> - - - - - - + isEnabled = "NO"> + + + + + + + + Void)?) -> Void)) { + guard let userActionCompletionHandler = self.userActionCompletionHandler else { return } + + self.userActionCompletionHandler = nil + + OnMainThread { + hudCompletion({ + OnMainThread { + userActionCompletionHandler(self.bookmark, true) + } + self.presentingViewController?.dismiss(animated: true, completion: nil) + }) + } + } + // MARK: - User actions @objc func userActionCancel() { let userActionCompletionHandler = self.userActionCompletionHandler @@ -553,80 +581,72 @@ class BookmarkViewController: StaticTableViewController { save(hudCompletion: hudCompletion) } - func save(hudCompletion: @escaping (((() -> Void)?) -> Void)) { - guard let bookmark = self.bookmark else { return } - - if isBookmarkComplete(bookmark: bookmark) { - bookmark.authenticationDataStorage = .keychain // Commit auth changes to keychain - - let connection = instantiateConnection(for: bookmark) - - connection.connect { [weak self] (error, issue) in - if let strongSelf = self { - if error == nil { - bookmark.userDisplayName = connection.loggedInUser?.displayName - - connection.disconnect(completionHandler: { - switch strongSelf.mode { - case .create: - // Add bookmark - OCBookmarkManager.shared.addBookmark(bookmark) + func updateBookmark(bookmark: OCBookmark) { + originalBookmark?.setValuesFrom(bookmark) + if let originalBookmark = originalBookmark, !OCBookmarkManager.shared.updateBookmark(originalBookmark) { + Log.error("Changes to \(originalBookmark) not saved as it's not tracked by OCBookmarkManager!") + } + } - case .edit: - // Update original bookmark - self?.originalBookmark?.setValuesFrom(bookmark) - if let originalBookmark = self?.originalBookmark, !OCBookmarkManager.shared.updateBookmark(originalBookmark) { - Log.error("Changes to \(originalBookmark) not saved as it's not tracked by OCBookmarkManager!") + func save(hudCompletion: @escaping (((() -> Void)?) -> Void)) { + guard let bookmark = self.bookmark else { return } + + if isBookmarkComplete(bookmark: bookmark) { + bookmark.authenticationDataStorage = .keychain // Commit auth changes to keychain + let connection = instantiateConnection(for: bookmark) + + connection.connect { [weak self] (error, issue) in + if let strongSelf = self { + if error == nil { + bookmark.userDisplayName = connection.loggedInUser?.displayName + + connection.disconnect(completionHandler: { + switch strongSelf.mode { + case .create: + // Add bookmark + OCBookmarkManager.shared.addBookmark(bookmark) + + case .edit: + // Update original bookmark + self?.updateBookmark(bookmark: bookmark) } - } - - let userActionCompletionHandler = strongSelf.userActionCompletionHandler - strongSelf.userActionCompletionHandler = nil + strongSelf.completeAndDismiss(with: hudCompletion) + }) + } else { OnMainThread { hudCompletion({ - OnMainThread { - userActionCompletionHandler?(bookmark, true) + if let issue = issue { + self?.bookmark?.authenticationData = nil + + IssuesCardViewController.present(on: strongSelf, issue: issue, completion: { [weak self, weak issue] (response) in + switch response { + case .cancel: + issue?.reject() + + case .approve: + issue?.approve() + self?.handleContinue() + + case .dismiss: break + } + }) + } else { + strongSelf.presentingViewController?.dismiss(animated: true, completion: nil) } - strongSelf.presentingViewController?.dismiss(animated: true, completion: nil) }) } - - }) - } else { - OnMainThread { - hudCompletion({ - if let issue = issue { - self?.bookmark?.authenticationData = nil - - IssuesCardViewController.present(on: strongSelf, issue: issue, completion: { [weak self, weak issue] (response) in - switch response { - case .cancel: - issue?.reject() - - case .approve: - issue?.approve() - self?.handleContinue() - - case .dismiss: break - } - }) - } else { - strongSelf.presentingViewController?.dismiss(animated: true, completion: nil) - } - }) } } } + } else { + hudCompletion({ [weak self] in + if let strongSelf = self { + strongSelf.handleContinue() + } + }) } - } else { - hudCompletion({ [weak self] in - if let strongSelf = self { - strongSelf.handleContinue() - } - }) } - } // MARK: - Update section and row composition func composeSectionsAndRows(animated: Bool = true, completion: (() -> Void)? = nil) { @@ -918,6 +938,13 @@ extension OCClassSettingsIdentifier { extension OCClassSettingsKey { static let bookmarkDefaultURL = OCClassSettingsKey("default-url") static let bookmarkURLEditable = OCClassSettingsKey("url-editable") + static let prepopulation = OCClassSettingsKey("prepopulation") +} + +enum BookmarkPrepopulationMethod : String { + case doNot + case streaming + case split } extension BookmarkViewController : OCClassSettingsSupport { @@ -947,6 +974,27 @@ extension BookmarkViewController : OCClassSettingsSupport { .description : "Controls whether the server URL in the text field during the creation of new bookmarks can be changed.", .category : "Bookmarks", .status : OCClassSettingsKeyStatus.supported + ], + + .prepopulation : [ + .type : OCClassSettingsMetadataType.string, + .description : "Controls prepopulation of the local database with the full item set during account setup.", + .category : "Bookmarks", + .status : OCClassSettingsKeyStatus.supported, + .possibleValues : [ + [ + OCClassSettingsMetadataKey.description : "No prepopulation. Request the contents of every folder individually.", + OCClassSettingsMetadataKey.value : BookmarkPrepopulationMethod.doNot.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "Parse the prepopulation metadata while receiving it.", + OCClassSettingsMetadataKey.value : BookmarkPrepopulationMethod.streaming.rawValue + ], + [ + OCClassSettingsMetadataKey.description : "Parse the prepopulation metadata after receiving it as a whole.", + OCClassSettingsMetadataKey.value : BookmarkPrepopulationMethod.split.rawValue + ] + ] ] ] } diff --git a/ownCloud/Client/ClientRootViewController.swift b/ownCloud/Client/ClientRootViewController.swift index 7797ef54c..3ddd30e80 100644 --- a/ownCloud/Client/ClientRootViewController.swift +++ b/ownCloud/Client/ClientRootViewController.swift @@ -196,10 +196,10 @@ class ClientRootViewController: UITabBarController, BookmarkContainer, ToolAndTa core?.vault.keyValueStore?.storeObject(nil, forKey: .coreSkipAvailableOfflineKey) }, completionHandler: { (core, error) in if error == nil { - // Set up FP standby - if let core = core { - self.fpServiceStandby = OCFileProviderServiceStandby(core: core) - self.fpServiceStandby?.start() + // Start FP standby in 5 seconds regardless of connnection status + // (or below: after it's clear that authentication worked) + OnBackgroundQueue(async: true, after: 5.0) { [weak self] in + self?.startFPServiceStandbyIfNotRunning() } // Core is ready @@ -209,6 +209,13 @@ class ClientRootViewController: UITabBarController, BookmarkContainer, ToolAndTa OnMainThread { [weak self] () in self?.connectionStatusObservation = core?.observe(\OCCore.connectionStatus, options: [.initial], changeHandler: { [weak self] (_, _) in self?.updateConnectionStatusSummary() + + if let connectionStatus = self?.core?.connectionStatus, + connectionStatus == .online { + // Start FP service standby after it's clear that authentication worked + // (or above: after 5 seconds regardless of connnection status) + self?.startFPServiceStandbyIfNotRunning() + } }) } } else { @@ -224,6 +231,18 @@ class ClientRootViewController: UITabBarController, BookmarkContainer, ToolAndTa }) } + func startFPServiceStandbyIfNotRunning() { + // Set up FP standby + OCSynchronized(self) { + if let core = core, + core.state == .starting || core.state == .running, + self.fpServiceStandby == nil { + self.fpServiceStandby = OCFileProviderServiceStandby(core: core) + self.fpServiceStandby?.start() + } + } + } + var pushTransition : PushTransitionDelegate? override func viewDidLoad() { diff --git a/ownCloud/Release Notes/ReleaseNotes.plist b/ownCloud/Release Notes/ReleaseNotes.plist index 8877acac8..944f84cad 100644 --- a/ownCloud/Release Notes/ReleaseNotes.plist +++ b/ownCloud/Release Notes/ReleaseNotes.plist @@ -1502,6 +1502,53 @@ Added an optional "Wait for completion" option to the "Save File& + + Version + 11.9.0 + ReleaseNotes + + + Title + Biometrical Authentication Button + Subtitle + Added a biometrical authentication button to the passcode unlock view. The biomatrical cancel button enables passcode unlock as fallback. + Type + New + ImageName + lock.shield + + + Title + Rename Account + Subtitle + Renaming an account does no longer need a re-authentication + Type + Fix + ImageName + pencil + + + Title + Media Playback + Subtitle + Fixes a bug where media playback failed + Type + Fix + ImageName + wrench + + + Title + Faster Account Scan + Subtitle + We improved the time for the initial scan after setting up a new account + Type + Fix + ImageName + wrench + + + diff --git a/ownCloud/Resources/Assets.xcassets/Contents.json b/ownCloud/Resources/Assets.xcassets/Contents.json index da4a164c9..73c00596a 100644 --- a/ownCloud/Resources/Assets.xcassets/Contents.json +++ b/ownCloud/Resources/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/ownCloud/Resources/Assets.xcassets/biometrical-faceid.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/biometrical-faceid.imageset/Contents.json new file mode 100644 index 000000000..5791c7fb4 --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/biometrical-faceid.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "biometrical-faceid.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ownCloud/Resources/Assets.xcassets/biometrical-faceid.imageset/biometrical-faceid.pdf b/ownCloud/Resources/Assets.xcassets/biometrical-faceid.imageset/biometrical-faceid.pdf new file mode 100644 index 000000000..fc092104f Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/biometrical-faceid.imageset/biometrical-faceid.pdf differ diff --git a/ownCloud/Resources/Assets.xcassets/biometrical-touchid.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/biometrical-touchid.imageset/Contents.json new file mode 100644 index 000000000..b7a7647cb --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/biometrical-touchid.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "biometrical.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/ownCloud/Resources/Assets.xcassets/biometrical-touchid.imageset/biometrical.pdf b/ownCloud/Resources/Assets.xcassets/biometrical-touchid.imageset/biometrical.pdf new file mode 100644 index 000000000..107c61243 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/biometrical-touchid.imageset/biometrical.pdf differ diff --git a/ownCloud/Resources/ar.lproj/Localizable.strings b/ownCloud/Resources/ar.lproj/Localizable.strings index 1a986fbce..0bf4e5781 100644 Binary files a/ownCloud/Resources/ar.lproj/Localizable.strings and b/ownCloud/Resources/ar.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/de.lproj/Localizable.strings b/ownCloud/Resources/de.lproj/Localizable.strings index 12894c8a9..db8e732dc 100644 Binary files a/ownCloud/Resources/de.lproj/Localizable.strings and b/ownCloud/Resources/de.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/el.lproj/InfoPlist.strings b/ownCloud/Resources/el.lproj/InfoPlist.strings index 8b36560ba..453295626 100644 Binary files a/ownCloud/Resources/el.lproj/InfoPlist.strings and b/ownCloud/Resources/el.lproj/InfoPlist.strings differ diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index da32cf161..5b6e79e6f 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -58,6 +58,10 @@ "Fetching user information…" = "Fetching user information…"; "Updating connection…" = "Updating connection…"; +"Preparing account" = "Preparing account"; +"Please wait…" = "Please wait…"; +"Skip" = "Skip"; + "Missing hostname" = "Missing hostname"; "The entered URL does not include a hostname." = "The entered URL does not include a hostname."; "Add account" = "Add account"; @@ -673,6 +677,7 @@ "Previous" = "Previous"; "Favorite" = "Favorite"; "Cut" = "Cut"; +"Paste" = "Paste"; "Play/Pause" = "Play/Pause"; "Skip Back" = "Skip Back"; "Skip Ahead" = "Skip Ahead"; diff --git a/ownCloud/Resources/es.lproj/Localizable.strings b/ownCloud/Resources/es.lproj/Localizable.strings index 8efac939c..257c36056 100644 Binary files a/ownCloud/Resources/es.lproj/Localizable.strings and b/ownCloud/Resources/es.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/fr.lproj/Localizable.strings b/ownCloud/Resources/fr.lproj/Localizable.strings index 93e904786..cb12c0d0f 100644 Binary files a/ownCloud/Resources/fr.lproj/Localizable.strings and b/ownCloud/Resources/fr.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/gl.lproj/Localizable.strings b/ownCloud/Resources/gl.lproj/Localizable.strings index 0e8ffee42..65b801cc0 100644 Binary files a/ownCloud/Resources/gl.lproj/Localizable.strings and b/ownCloud/Resources/gl.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/he.lproj/Localizable.strings b/ownCloud/Resources/he.lproj/Localizable.strings index 5bf913b08..0e771ebb0 100644 Binary files a/ownCloud/Resources/he.lproj/Localizable.strings and b/ownCloud/Resources/he.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/pt-BR.lproj/Localizable.strings b/ownCloud/Resources/pt-BR.lproj/Localizable.strings index e8d13f0c4..6211b91cc 100644 Binary files a/ownCloud/Resources/pt-BR.lproj/Localizable.strings and b/ownCloud/Resources/pt-BR.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/ru.lproj/Localizable.strings b/ownCloud/Resources/ru.lproj/Localizable.strings index 26eeac097..5eaf6914f 100644 Binary files a/ownCloud/Resources/ru.lproj/Localizable.strings and b/ownCloud/Resources/ru.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/sq.lproj/Localizable.strings b/ownCloud/Resources/sq.lproj/Localizable.strings index 079786a69..ddc4dda18 100644 Binary files a/ownCloud/Resources/sq.lproj/Localizable.strings and b/ownCloud/Resources/sq.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/th-TH.lproj/Localizable.strings b/ownCloud/Resources/th-TH.lproj/Localizable.strings index 70cf4e2a9..a3ece4f80 100644 Binary files a/ownCloud/Resources/th-TH.lproj/Localizable.strings and b/ownCloud/Resources/th-TH.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/zh_TW.lproj/Localizable.strings b/ownCloud/Resources/zh_TW.lproj/Localizable.strings index 1d5d4af2a..745906f54 100644 Binary files a/ownCloud/Resources/zh_TW.lproj/Localizable.strings and b/ownCloud/Resources/zh_TW.lproj/Localizable.strings differ diff --git a/ownCloud/Server List/ServerListTableViewController.swift b/ownCloud/Server List/ServerListTableViewController.swift index cef56d2b2..bb43aa2f9 100644 --- a/ownCloud/Server List/ServerListTableViewController.swift +++ b/ownCloud/Server List/ServerListTableViewController.swift @@ -722,7 +722,7 @@ class ServerListTableViewController: UITableViewController, Themeable, StateRest } if tableView.isEditing { - self.showBookmarkUI(edit: bookmark) + self.showBookmarkUI(edit: bookmark, removeAuthDataFromCopy: false) } else { self.connect(to: bookmark, lastVisibleItemId: nil, animated: true) self.tableView.deselectRow(at: indexPath, animated: true) @@ -756,7 +756,7 @@ class ServerListTableViewController: UITableViewController, Themeable, StateRest menuItems.append(openWindow) } let edit = UIAction(title: "Edit".localized, image: UIImage(systemName: "gear")) { _ in - self.showBookmarkUI(edit: bookmark) + self.showBookmarkUI(edit: bookmark, removeAuthDataFromCopy: false) } if VendorServices.shared.canEditAccount { menuItems.append(edit) @@ -853,7 +853,7 @@ class ServerListTableViewController: UITableViewController, Themeable, StateRest let editRowAction = UITableViewRowAction(style: .normal, title: "Edit".localized, handler: { [weak self] (_, indexPath) in if let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { - self?.showBookmarkUI(edit: bookmark) + self?.showBookmarkUI(edit: bookmark, removeAuthDataFromCopy: false) } }) editRowAction.backgroundColor = .blue diff --git a/ownCloud/Static Login/Interface/StaticLoginSetupViewController.swift b/ownCloud/Static Login/Interface/StaticLoginSetupViewController.swift index e9d4786c9..ec3f65406 100644 --- a/ownCloud/Static Login/Interface/StaticLoginSetupViewController.swift +++ b/ownCloud/Static Login/Interface/StaticLoginSetupViewController.swift @@ -50,6 +50,10 @@ class StaticLoginSetupViewController : StaticLoginStepViewController { fatalError("init(coder:) has not been implemented") } + var askForUsernameFirst : Bool { + return OCServerLocator.useServerLocatorIdentifier != nil + } + override func viewDidLoad() { super.viewDidLoad() @@ -62,7 +66,11 @@ class StaticLoginSetupViewController : StaticLoginStepViewController { self.addSection(onboardingSection()) } } else { - proceedWithLogin() + if askForUsernameFirst { + self.addSection(accountEntryMaskSection()) + } else { + proceedWithLogin() + } } } @@ -117,20 +125,53 @@ class StaticLoginSetupViewController : StaticLoginStepViewController { return onboardingSection } + func accountEntryMaskSection() -> StaticTableViewSection { + var accountEntryMaskSection : StaticTableViewSection + + accountEntryMaskSection = StaticTableViewSection(headerTitle: nil, identifier: "accountEntryMaskSection") + accountEntryMaskSection.addStaticHeader(title: profile.welcome!, message: "Enter username".localized) + + accountEntryMaskSection.add(row: StaticTableViewRow(textFieldWithAction: { [weak self] (row, _, type) in + if type == .didBegin, let cell = row.cell, let indexPath = self?.tableView.indexPath(for: cell) { + self?.tableView.scrollToRow(at: indexPath, at: .top, animated: true) + } + if let value = row.value as? String { + self?.username = value + } + }, placeholder: "Username".localized, value: username ?? "", keyboardType: .asciiCapable, autocorrectionType: .no, autocapitalizationType: .none, returnKeyType: .continue, identifier: "username")) + + if VendorServices.shared.canAddAccount, OCBookmarkManager.shared.bookmarks.count > 0 { + let (proceedButton, cancelButton) = accountEntryMaskSection.addButtonFooter(proceedLabel: "Proceed".localized, proceedItemStyle: .welcome, cancelLabel: "Cancel".localized) + proceedButton?.addTarget(self, action: #selector(self.proceedWithLogin), for: .touchUpInside) + cancelButton?.addTarget(self, action: #selector(self.cancel(_:)), for: .touchUpInside) + } else { + let (proceedButton, _) = accountEntryMaskSection.addButtonFooter(proceedLabel: "Proceed".localized, proceedItemStyle: .welcome, cancelLabel: nil) + proceedButton?.addTarget(self, action: #selector(self.proceedWithLogin), for: .touchUpInside) + } + + return accountEntryMaskSection + } + func loginMaskSection() -> StaticTableViewSection { var loginMaskSection : StaticTableViewSection loginMaskSection = StaticTableViewSection(headerTitle: nil, identifier: "loginMaskSection") loginMaskSection.addStaticHeader(title: profile.welcome!, message: profile.promptForPasswordAuth) - loginMaskSection.add(row: StaticTableViewRow(textFieldWithAction: { [weak self] (row, _, type) in + let userNameRow = StaticTableViewRow(textFieldWithAction: { [weak self] (row, _, type) in if type == .didBegin, let cell = row.cell, let indexPath = self?.tableView.indexPath(for: cell) { self?.tableView.scrollToRow(at: indexPath, at: .top, animated: true) } if let value = row.value as? String { self?.username = value } - }, placeholder: "Username".localized, keyboardType: .asciiCapable, autocorrectionType: .no, autocapitalizationType: .none, returnKeyType: .continue, identifier: "username", borderStyle: .roundedRect)) + }, placeholder: "Username".localized, value: self.username ?? "", keyboardType: .asciiCapable, autocorrectionType: .no, autocapitalizationType: .none, returnKeyType: .continue, identifier: "username", borderStyle: .roundedRect) + + if let username = self.username, username.count > 0 { + userNameRow.enabled = false + } + + loginMaskSection.add(row: userNameRow) passwordRow = StaticTableViewRow(secureTextFieldWithAction: { [weak self] (row, _, type) in if type == .didBegin, let cell = row.cell, let indexPath = self?.tableView.indexPath(for: cell) { @@ -320,6 +361,9 @@ class StaticLoginSetupViewController : StaticLoginStepViewController { return } + if let accountEntryMaskSection = self.sectionForIdentifier("accountEntryMaskSection") { + self.removeSection(accountEntryMaskSection) + } if let urlSection = self.sectionForIdentifier("urlSection") { self.removeSection(urlSection) } @@ -352,6 +396,8 @@ class StaticLoginSetupViewController : StaticLoginStepViewController { if OCAuthenticationMethod.registeredAuthenticationMethod(forIdentifier: authMethodIdentifier)?.type == .passphrase { options[.usernameKey] = username ?? "" options[.passphraseKey] = password ?? "" + } else if askForUsernameFirst, let username = username { + options[.usernameKey] = username } options[.presentingViewControllerKey] = self @@ -467,7 +513,9 @@ class StaticLoginSetupViewController : StaticLoginStepViewController { guard let bookmark = self.bookmark else { return } let connection = instantiateConnection(for: bookmark) - connection.prepareForSetup(options: nil, completionHandler: { (connectionIssue, _, _, preferredAuthenticationMethods) in + connection.prepareForSetup(options: ((username != nil) ? [ + .userName : username! + ] : nil), completionHandler: { (connectionIssue, _, _, preferredAuthenticationMethods) in var proceed : Bool = true if let issue = connectionIssue { @@ -499,7 +547,11 @@ class StaticLoginSetupViewController : StaticLoginStepViewController { self.addSection(self.onboardingSection()) } } else { - self.cancel(nil) + if self.askForUsernameFirst { + self.addSection(self.accountEntryMaskSection()) + } else { + self.cancel(nil) + } } } }) @@ -545,6 +597,10 @@ class StaticLoginSetupViewController : StaticLoginStepViewController { if self.sectionForIdentifier("tokenMaskSection") == nil { self.addSection(self.tokenMaskSection()) } + + if self.username != nil { + self.startAuthentication(nil) + } } if self.profile.isOnboardingEnabled, self.sectionForIdentifier("onboardingSection") == nil { diff --git a/ownCloud/Static Login/Interface/StaticLoginSingleAccountServerListViewController.swift b/ownCloud/Static Login/Interface/StaticLoginSingleAccountServerListViewController.swift index 3981767fd..4ff062240 100644 --- a/ownCloud/Static Login/Interface/StaticLoginSingleAccountServerListViewController.swift +++ b/ownCloud/Static Login/Interface/StaticLoginSingleAccountServerListViewController.swift @@ -284,7 +284,7 @@ class StaticLoginSingleAccountServerListViewController: ServerListTableViewContr tableView.deselectRow(at: indexPath, animated: true) switch actionRows[indexPath.row] { case .editLogin: - showBookmarkUI(edit: bookmark) + showBookmarkUI(edit: bookmark, removeAuthDataFromCopy: false) case .manageStorage: showBookmarkInfoUI(bookmark) diff --git a/ownCloud/Static Login/Interface/StaticLoginViewController.swift b/ownCloud/Static Login/Interface/StaticLoginViewController.swift index a88762eb7..6a0ba50c4 100644 --- a/ownCloud/Static Login/Interface/StaticLoginViewController.swift +++ b/ownCloud/Static Login/Interface/StaticLoginViewController.swift @@ -80,7 +80,7 @@ class StaticLoginViewController: UIViewController, Themeable, StateRestorationCo var toolbarShown : Bool = false { didSet { - if self.toolbarItems == nil, toolbarShown { + if self.toolbarItems == nil, toolbarShown, OCBookmarkManager.shared.bookmarks.count == 0 { let settingsBarButtonItem = UIBarButtonItem(title: "Settings".localized, style: UIBarButtonItem.Style.plain, target: self, action: #selector(settings)) settingsBarButtonItem.accessibilityIdentifier = "settingsBarButtonItem" @@ -248,6 +248,7 @@ class StaticLoginViewController: UIViewController, Themeable, StateRestorationCo firstViewController = self.buildBookmarkSelector() } else if let firstProfile = loginBundle.profiles.first { // Setup flow + self.toolbarShown = true if loginBundle.profiles.count > 1 { // Profile setup selector firstViewController = buildProfileSetupSelector(title: firstProfile.welcome!) diff --git a/ownCloudAppShared/AppLock/AppLockManager.swift b/ownCloudAppShared/AppLock/AppLockManager.swift index 19736b4dc..1b2f00990 100644 --- a/ownCloudAppShared/AppLock/AppLockManager.swift +++ b/ownCloudAppShared/AppLock/AppLockManager.swift @@ -80,6 +80,14 @@ public class AppLockManager: NSObject { self.userDefaults.set(newValue, forKey: "applock-locked-until-date") } } + private var biometricalAuthenticationSucceeded: Bool { + get { + return userDefaults.bool(forKey: "applock-biometrical-authentication-succeeded") + } + set(newValue) { + self.userDefaults.set(newValue, forKey: "applock-biometrical-authentication-succeeded") + } + } private let maximumPasscodeAttempts: Int = 3 private let powBaseDelay: Double = 1.5 @@ -146,7 +154,7 @@ public class AppLockManager: NSObject { lockscreenOpen = true // Show biometrical - if !forceShow, !self.shouldDisplayCountdown { + if !forceShow, !self.shouldDisplayCountdown, self.biometricalAuthenticationSucceeded { showBiometricalAuthenticationInterface(context: context) } } @@ -187,6 +195,8 @@ public class AppLockManager: NSObject { private var passcodeControllerByWindow : NSMapTable = NSMapTable.weakToStrongObjects() private var applockWindowByWindow : NSMapTable = NSMapTable.weakToStrongObjects() + open var biometricCancelLabel : String? + open var cancelAction : (() -> Void)? open var successAction : (() -> Void)? @@ -282,7 +292,12 @@ public class AppLockManager: NSObject { func passwordViewController() -> PasscodeViewController { var passcodeViewController : PasscodeViewController - passcodeViewController = PasscodeViewController(completionHandler: { (viewController: PasscodeViewController, passcode: String) in + passcodeViewController = PasscodeViewController(biometricalHandler: { (passcodeViewController) in + if !self.shouldDisplayCountdown { + let context = LAContext() + self.showBiometricalAuthenticationInterface(context: context) + } + }, completionHandler: { (viewController: PasscodeViewController, passcode: String) in self.attemptUnlock(with: passcode, passcodeViewController: viewController) }, requiredLength: AppLockManager.shared.passcode?.count ?? AppLockSettings.shared.requiredPasscodeDigits) @@ -452,7 +467,7 @@ public class AppLockManager: NSObject { } } - context.localizedCancelTitle = "Enter code".localized + context.localizedCancelTitle = biometricCancelLabel ?? "Enter code".localized context.localizedFallbackTitle = "" self.biometricalAuthenticationInterfaceShown = true @@ -461,6 +476,7 @@ public class AppLockManager: NSObject { self.biometricalAuthenticationInterfaceShown = false if success { + self.biometricalAuthenticationSucceeded = true // Fill the passcode dots OnMainThread { self.performPasscodeViewControllerUpdates { (passcodeViewController) in @@ -478,6 +494,7 @@ public class AppLockManager: NSObject { self.attemptUnlock(with: self.passcode) } } else { + self.biometricalAuthenticationSucceeded = false if let error = error { switch error { case LAError.biometryLockout: diff --git a/ownCloudAppShared/AppLock/PasscodeViewController.swift b/ownCloudAppShared/AppLock/PasscodeViewController.swift index 7fb5ae2b1..de640f6ac 100644 --- a/ownCloudAppShared/AppLock/PasscodeViewController.swift +++ b/ownCloudAppShared/AppLock/PasscodeViewController.swift @@ -17,8 +17,11 @@ */ import UIKit +import ownCloudApp +import LocalAuthentication public typealias PasscodeViewControllerCancelHandler = ((_ passcodeViewController: PasscodeViewController) -> Void) +public typealias PasscodeViewControllerBiometricalHandler = ((_ passcodeViewController: PasscodeViewController) -> Void) public typealias PasscodeViewControllerCompletionHandler = ((_ passcodeViewController: PasscodeViewController, _ passcode: String) -> Void) public class PasscodeViewController: UIViewController, Themeable { @@ -39,6 +42,8 @@ public class PasscodeViewController: UIViewController, Themeable { @IBOutlet private var keypadButtons: [ThemeRoundedButton]? @IBOutlet private var deleteButton: ThemeButton? @IBOutlet public var cancelButton: ThemeButton? + @IBOutlet public var biometricalButton: ThemeButton? + @IBOutlet public var biometricalImageView: UIImageView? @IBOutlet public var compactHeightPasscodeTextField: UITextField? // MARK: - Properties @@ -109,6 +114,15 @@ public class PasscodeViewController: UIViewController, Themeable { } } + var biometricalButtonHidden: Bool = false { + didSet { + biometricalButton?.isEnabled = biometricalButtonHidden + biometricalButton?.isHidden = !biometricalButtonHidden + biometricalImageView?.isHidden = !biometricalButtonHidden + biometricalImageView?.image = LAContext().biometricsAuthenticationImage() + } + } + var hasCompactHeight: Bool { if self.traitCollection.verticalSizeClass == .compact { return true @@ -119,11 +133,13 @@ public class PasscodeViewController: UIViewController, Themeable { // MARK: - Handlers public var cancelHandler: PasscodeViewControllerCancelHandler? + public var biometricalHandler: PasscodeViewControllerBiometricalHandler? public var completionHandler: PasscodeViewControllerCompletionHandler? // MARK: - Init - public init(cancelHandler: PasscodeViewControllerCancelHandler? = nil, completionHandler: @escaping PasscodeViewControllerCompletionHandler, hasCancelButton: Bool = true, keypadButtonsEnabled: Bool = true, requiredLength: Int) { + public init(cancelHandler: PasscodeViewControllerCancelHandler? = nil, biometricalHandler: PasscodeViewControllerBiometricalHandler? = nil, completionHandler: @escaping PasscodeViewControllerCompletionHandler, hasCancelButton: Bool = true, keypadButtonsEnabled: Bool = true, requiredLength: Int) { self.cancelHandler = cancelHandler + self.biometricalHandler = biometricalHandler self.completionHandler = completionHandler self.keypadButtonsEnabled = keypadButtonsEnabled self.cancelButtonHidden = hasCancelButton @@ -157,7 +173,11 @@ public class PasscodeViewController: UIViewController, Themeable { self.screenBlurringEnabled = { self.screenBlurringEnabled }() self.errorMessageLabel?.minimumScaleFactor = 0.5 self.errorMessageLabel?.adjustsFontSizeToFitWidth = true + self.biometricalButtonHidden = !(!AppLockSettings.shared.biometricalSecurityEnabled || self.cancelButtonHidden) updateKeypadButtons() + if let biometricalSecurityName = LAContext().supportedBiometricsAuthenticationName() { + self.biometricalButton?.accessibilityLabel = biometricalSecurityName + } if #available(iOS 13.4, *) { for button in keypadButtons! { @@ -165,6 +185,7 @@ public class PasscodeViewController: UIViewController, Themeable { } PointerEffect.install(on: cancelButton!, effectStyle: .highlight) PointerEffect.install(on: deleteButton!, effectStyle: .highlight) + PointerEffect.install(on: biometricalButton!, effectStyle: .highlight) } } @@ -283,6 +304,10 @@ public class PasscodeViewController: UIViewController, Themeable { cancelHandler?(self) } + @IBAction func biometricalAction(_ sender: UIButton) { + biometricalHandler?(self) + } + // MARK: - Themeing public override var preferredStatusBarStyle : UIStatusBarStyle { if VendorServices.shared.isBranded { @@ -311,6 +336,8 @@ public class PasscodeViewController: UIViewController, Themeable { deleteButton?.themeColorCollection = ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: collection.neutralColors.normal.background, background: .clear)) + biometricalImageView?.tintColor = collection.tintColor + cancelButton?.applyThemeCollection(collection, itemStyle: .defaultForItem) } } diff --git a/ownCloudAppShared/AppLock/PasscodeViewController.xib b/ownCloudAppShared/AppLock/PasscodeViewController.xib index 4878a3b6b..3f25d1817 100644 --- a/ownCloudAppShared/AppLock/PasscodeViewController.xib +++ b/ownCloudAppShared/AppLock/PasscodeViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -11,6 +11,8 @@ + + @@ -199,6 +201,21 @@ + + + + + + + + + + + @@ -348,6 +372,9 @@ + + + @@ -370,4 +397,7 @@ + + + diff --git a/ownCloudAppShared/UIKit Extension/LAContext+Extension.swift b/ownCloudAppShared/UIKit Extension/LAContext+Extension.swift index 8c620920e..8233889af 100644 --- a/ownCloudAppShared/UIKit Extension/LAContext+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/LAContext+Extension.swift @@ -17,11 +17,11 @@ */ import LocalAuthentication +import UIKit extension LAContext { public func supportedBiometricsAuthenticationName() -> String? { - if canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { switch self.biometryType { case .faceID : return "Face ID".localized @@ -30,5 +30,24 @@ extension LAContext { } } return nil - } + } + + public func biometricsAuthenticationImage() -> UIImage? { + if canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { + switch self.biometryType { + case .faceID : if #available(iOSApplicationExtension 13.0, *) { + return UIImage(systemName: "faceid") + } else { + return UIImage(named: "biometrical-faceid") + } + case .touchID: if #available(iOSApplicationExtension 13.0, *) { + return UIImage(systemName: "touchid") + } else { + return UIImage(named: "biometrical-touchid") + } + case .none: return nil + } + } + return nil + } } diff --git a/ownCloudAppShared/User Interface/Progress/ProgressIndicatorViewController.swift b/ownCloudAppShared/User Interface/Progress/ProgressIndicatorViewController.swift index a7196258d..1ebe6c794 100644 --- a/ownCloudAppShared/User Interface/Progress/ProgressIndicatorViewController.swift +++ b/ownCloudAppShared/User Interface/Progress/ProgressIndicatorViewController.swift @@ -1,5 +1,5 @@ // -// FullProgressViewController.swift +// ProgressIndicatorViewController.swift // ownCloud Share Extension // // Created by Felix Schwarz on 07.08.20. @@ -23,22 +23,81 @@ open class ProgressIndicatorViewController: UIViewController, Themeable { open var cancelHandler : (() -> Void)? open var progressView : UIProgressView + open var activityIndicator : UIActivityIndicatorView open var label : UILabel + open var titleLabel : UILabel? open var cancelButton : UIButton? - public init(initialProgressLabel: String?, cancelLabel: String? = nil, cancelHandler: (() -> Void)? = nil) { + var progressMessageObservation : NSKeyValueObservation? + var progressValueObservation : NSKeyValueObservation? + open var progress : Progress? { + willSet { + progressMessageObservation?.invalidate() + progressMessageObservation = nil + + progressValueObservation?.invalidate() + progressValueObservation = nil + } + + didSet { + if progress != nil { + progressMessageObservation = progress?.observe(\Progress.localizedDescription, options: NSKeyValueObservingOptions.initial, changeHandler: { [weak self] progress, _ in + OnMainThread { + self?.label.text = progress.localizedDescription + } + }) + + progressValueObservation = progress?.observe(\Progress.fractionCompleted, options: NSKeyValueObservingOptions.initial, changeHandler: { [weak self] progress, _ in + OnMainThread { + if progress.isIndeterminate { + self?.activityIndicator.isHidden = false + self?.progressView.isHidden = true + self?.activityIndicator.startAnimating() + } else { + self?.activityIndicator.stopAnimating() + self?.activityIndicator.isHidden = true + self?.progressView.isHidden = false + } + } + }) + } + } + } + + public init(initialTitleLabel: String? = nil, initialProgressLabel: String?, progress : Progress?, cancelLabel: String? = nil, cancelHandler: (() -> Void)? = nil) { progressView = UIProgressView(progressViewStyle: .bar) progressView.translatesAutoresizingMaskIntoConstraints = false + if #available(iOS 13, *) { + activityIndicator = UIActivityIndicatorView(style: .large) + } else { + activityIndicator = UIActivityIndicatorView(style: .whiteLarge) + } + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + activityIndicator.isHidden = true + label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false super.init(nibName: nil, bundle: nil) + self.progress = progress + + if let initialTitleLabel = initialTitleLabel { + titleLabel = UILabel() + titleLabel?.translatesAutoresizingMaskIntoConstraints = false + titleLabel?.text = initialTitleLabel + titleLabel?.font = .preferredFont(forTextStyle: .title2) + + label.font = .preferredFont(forTextStyle: .subheadline) + } + if let initialProgressLabel = initialProgressLabel { label.text = initialProgressLabel } + self.cancelHandler = cancelHandler + if cancelHandler != nil { cancelButton = ThemeButton(type: .system) cancelButton?.translatesAutoresizingMaskIntoConstraints = false @@ -63,14 +122,20 @@ open class ProgressIndicatorViewController: UIViewController, Themeable { centerView.addSubview(progressView) centerView.addSubview(label) + if let titleLabel = titleLabel { + centerView.addSubview(titleLabel) + } + if let cancelButton = cancelButton { centerView.addSubview(cancelButton) } rootView.addSubview(centerView) + rootView.addSubview(activityIndicator) let outerSpacing : CGFloat = 10 - let labelProgressBarSpacing : CGFloat = 15 + let titleLabelSpacing : CGFloat = 5 + let labelProgressBarSpacing : CGFloat = 20 let cancelProgressBarSpacing : CGFloat = 40 let progressBarWidth : CGFloat = 280 @@ -83,7 +148,6 @@ open class ProgressIndicatorViewController: UIViewController, Themeable { label.centerXAnchor.constraint(equalTo: centerView.centerXAnchor), - label.topAnchor.constraint(equalTo: centerView.topAnchor, constant: outerSpacing), progressView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: labelProgressBarSpacing), centerView.leftAnchor.constraint(greaterThanOrEqualTo: rootView.leftAnchor, constant: outerSpacing), @@ -92,9 +156,28 @@ open class ProgressIndicatorViewController: UIViewController, Themeable { centerView.centerXAnchor.constraint(equalTo: rootView.centerXAnchor), centerView.centerYAnchor.constraint(equalTo: rootView.centerYAnchor), - centerView.widthAnchor.constraint(equalToConstant: progressBarWidth) + centerView.widthAnchor.constraint(equalToConstant: progressBarWidth), + + activityIndicator.centerXAnchor.constraint(equalTo: centerView.centerXAnchor), + activityIndicator.bottomAnchor.constraint(equalTo: centerView.topAnchor, constant:-labelProgressBarSpacing), + activityIndicator.widthAnchor.constraint(equalToConstant: 20), + activityIndicator.heightAnchor.constraint(equalToConstant: 20) ] + if let titleLabel = titleLabel { + constraints.append(contentsOf: [ + titleLabel.topAnchor.constraint(equalTo: centerView.topAnchor, constant: outerSpacing), + titleLabel.centerXAnchor.constraint(greaterThanOrEqualTo: centerView.centerXAnchor), + titleLabel.leftAnchor.constraint(greaterThanOrEqualTo: centerView.leftAnchor), + titleLabel.rightAnchor.constraint(lessThanOrEqualTo: centerView.rightAnchor), + label.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: titleLabelSpacing) + ]) + } else { + constraints.append(contentsOf: [ + label.topAnchor.constraint(equalTo: centerView.topAnchor, constant: outerSpacing), + ]) + } + if let cancelButton = cancelButton { constraints.append(contentsOf: [ cancelButton.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: cancelProgressBarSpacing), @@ -132,8 +215,10 @@ open class ProgressIndicatorViewController: UIViewController, Themeable { self.view.backgroundColor = collection.tableBackgroundColor self.progressView.applyThemeCollection(collection) + self.titleLabel?.applyThemeCollection(collection, itemStyle: .title, itemState: .normal) self.label.applyThemeCollection(collection) self.cancelButton?.applyThemeCollection(collection) + self.activityIndicator.style = collection.activityIndicatorViewStyle } open func update(progress: Float? = nil, text: String? = nil) { diff --git a/ownCloudAppShared/User Interface/Theme/UI/ThemeTableViewCell.swift b/ownCloudAppShared/User Interface/Theme/UI/ThemeTableViewCell.swift index 12e8a8831..919f8c732 100644 --- a/ownCloudAppShared/User Interface/Theme/UI/ThemeTableViewCell.swift +++ b/ownCloudAppShared/User Interface/Theme/UI/ThemeTableViewCell.swift @@ -145,6 +145,10 @@ open class ThemeTableViewCell: UITableViewCell, Themeable { textColor = collection.tableRowColors.labelColor backgroundColor = collection.tableRowColors.backgroundColor + case .text: + textColor = collection.tableRowColors.labelColor + backgroundColor = collection.tableRowColors.backgroundColor + case .confirmation: textColor = collection.approvalColors.normal.foreground backgroundColor = collection.approvalColors.normal.background