diff --git a/.xcode-version b/.xcode-version index c3d10c59d..c27905ac3 100644 --- a/.xcode-version +++ b/.xcode-version @@ -1 +1 @@ -13.3.1 +13.4.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbe8ef1b..e06254fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +Changelog for ownCloud iOS Client [11.10.1] (2022-08-02) +======================================= +The following sections list the changes in ownCloud iOS Client 11.10.1 relevant to +ownCloud admins and users. + +[11.10.1]: https://github.com/owncloud/ios-app/compare/milestone/11.10.0...milestone/11.10.1 + +Summary +------- + +* Bugfix - (Branding) Biometrical Unlock in Share Sheet: [#1129](https://github.com/owncloud/ios-app/pull/1129) +* Bugfix - Show folder contents from cache when offline: [#1130](https://github.com/owncloud/ios-app/issues/1130) +* Bugfix - (Branding) Color Issues: [#1132](https://github.com/owncloud/ios-app/pull/1132) + +Details +------- + +* Bugfix - (Branding) Biometrical Unlock in Share Sheet: [#1129](https://github.com/owncloud/ios-app/pull/1129) + + Biometrical unlock in the share sheet does not work in some third party apps like Boxer. With new + branding parameters it is now possible to disable biometrical unlock in the share sheet or to + exclude specific apps. + + https://github.com/owncloud/ios-app/pull/1129 + +* Bugfix - Show folder contents from cache when offline: [#1130](https://github.com/owncloud/ios-app/issues/1130) + + With this fix the app shows the contents of the available folders when offline. + + https://github.com/owncloud/ios-app/issues/1130 + +* Bugfix - (Branding) Color Issues: [#1132](https://github.com/owncloud/ios-app/pull/1132) + + Fix some automatic color values, if the branding color is bright by checking the brightness of + the color. + + https://github.com/owncloud/ios-app/pull/1132 + Changelog for ownCloud iOS Client [11.10.0] (2022-05-18) ======================================= The following sections list the changes in ownCloud iOS Client 11.10.0 relevant to diff --git a/changelog/11.10.1_2022-08-02/1129 b/changelog/11.10.1_2022-08-02/1129 new file mode 100644 index 000000000..64f0996d0 --- /dev/null +++ b/changelog/11.10.1_2022-08-02/1129 @@ -0,0 +1,5 @@ +Bugfix: (Branding) Biometrical Unlock in Share Sheet + +Biometrical unlock in the share sheet does not work in some third party apps like Boxer. With new branding parameters it is now possible to disable biometrical unlock in the share sheet or to exclude specific apps. + +https://github.com/owncloud/ios-app/pull/1129 diff --git a/changelog/11.10.1_2022-08-02/1130 b/changelog/11.10.1_2022-08-02/1130 new file mode 100644 index 000000000..630ff061d --- /dev/null +++ b/changelog/11.10.1_2022-08-02/1130 @@ -0,0 +1,5 @@ +Bugfix: Show folder contents from cache when offline + +With this fix the app shows the contents of the available folders when offline. + +https://github.com/owncloud/ios-app/issues/1130 diff --git a/changelog/11.10.1_2022-08-02/1132 b/changelog/11.10.1_2022-08-02/1132 new file mode 100644 index 000000000..3275f1a31 --- /dev/null +++ b/changelog/11.10.1_2022-08-02/1132 @@ -0,0 +1,5 @@ +Bugfix: (Branding) Color Issues + +Fix some automatic color values, if the branding color is bright by checking the brightness of the color. + +https://github.com/owncloud/ios-app/pull/1132 diff --git a/fastlane/metadata-emm/en-US/release_notes.txt b/fastlane/metadata-emm/en-US/release_notes.txt index 5da11c0dc..ffd45f70d 100644 --- a/fastlane/metadata-emm/en-US/release_notes.txt +++ b/fastlane/metadata-emm/en-US/release_notes.txt @@ -1,9 +1,3 @@ -• UI fixes on iOS 15 -This version fixes some UI problems on iOS 15. - -• Shortcuts Action -The shortcuts action Delete Path Item did not provided configured accounts. - -• Increased Timeout for Copy Action -Timeout for Copy Action was increased up to 10 minutes. +• Available Offline Folders +Shows the contents of the available folders when offline. diff --git a/fastlane/metadata-owncloud-online/en-US/release_notes.txt b/fastlane/metadata-owncloud-online/en-US/release_notes.txt index 5da11c0dc..ffd45f70d 100644 --- a/fastlane/metadata-owncloud-online/en-US/release_notes.txt +++ b/fastlane/metadata-owncloud-online/en-US/release_notes.txt @@ -1,9 +1,3 @@ -• UI fixes on iOS 15 -This version fixes some UI problems on iOS 15. - -• Shortcuts Action -The shortcuts action Delete Path Item did not provided configured accounts. - -• Increased Timeout for Copy Action -Timeout for Copy Action was increased up to 10 minutes. +• Available Offline Folders +Shows the contents of the available folders when offline. diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 5da11c0dc..ffd45f70d 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,9 +1,3 @@ -• UI fixes on iOS 15 -This version fixes some UI problems on iOS 15. - -• Shortcuts Action -The shortcuts action Delete Path Item did not provided configured accounts. - -• Increased Timeout for Copy Action -Timeout for Copy Action was increased up to 10 minutes. +• Available Offline Folders +Shows the contents of the available folders when offline. diff --git a/ios-sdk b/ios-sdk index 6bcd960e1..57729a606 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 6bcd960e12f6beddddd0afea5027fd11281afc72 +Subproject commit 57729a60605cfc810c7e858d1875fc49006ec7f9 diff --git a/ownCloud Share Extension/Info.plist b/ownCloud Share Extension/Info.plist index d06c3a6ff..8b1fc44c7 100644 --- a/ownCloud Share Extension/Info.plist +++ b/ownCloud Share Extension/Info.plist @@ -46,6 +46,8 @@ group.com.owncloud.ios-app OCAppIdentifierPrefix $(AppIdentifierPrefix) + OCAppComponentIdentifier + shareExtension OCHasFileProvider OCKeychainAccessGroupIdentifier diff --git a/ownCloud Share Extension/ShareNavigationController.swift b/ownCloud Share Extension/ShareNavigationController.swift index 84d5e9fd2..42db1e411 100644 --- a/ownCloud Share Extension/ShareNavigationController.swift +++ b/ownCloud Share Extension/ShareNavigationController.swift @@ -35,6 +35,14 @@ class ShareNavigationController: AppExtensionNavigationController { self.setViewControllers([viewController], animated: false) } } + + override func willMove(toParent parent: UIViewController?) { + super.willMove(toParent: parent) + + OCAppIdentity.shared.hostAppBundleIdentifier = parent?.oc_hostAppBundleIdentifier + + Log.debug("Extension Host App Bundle ID: \(OCAppIdentity.shared.hostAppBundleIdentifier ?? "nil")") + } } extension UserInterfaceContext : UserInterfaceContextProvider { diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 496ac4819..72b9ed457 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -311,6 +311,8 @@ DC3BE0D82077BC5D002A0AC0 /* ownCloudSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239369782076110900BCE21A /* ownCloudSDK.framework */; }; DC3BE0DA2077BC6B002A0AC0 /* ownCloudSDK.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 239369782076110900BCE21A /* ownCloudSDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DC3BE0DF2077CC14002A0AC0 /* ClientRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3BE0DD2077CC13002A0AC0 /* ClientRootViewController.swift */; }; + DC3DDF06287E1C0800E5586D /* UIViewController+HostBundleID.m in Sources */ = {isa = PBXBuildFile; fileRef = DC3DDF03287E1AC200E5586D /* UIViewController+HostBundleID.m */; }; + DC3DDF07287E1C0E00E5586D /* UIViewController+HostBundleID.h in Headers */ = {isa = PBXBuildFile; fileRef = DC3DDF02287E1AC200E5586D /* UIViewController+HostBundleID.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC3DEC7B22AFA1F000F3352D /* DownloadItemsHUDViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3DEC7A22AFA1F000F3352D /* DownloadItemsHUDViewController.swift */; }; DC3F4522271A23A000ED2383 /* AcknowledgementsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3F4521271A23A000ED2383 /* AcknowledgementsTableViewController.swift */; }; DC4332002472E1B4002DC0E5 /* OCLicenseEMMProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = DC4331FE2472E1B4002DC0E5 /* OCLicenseEMMProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1239,6 +1241,8 @@ DC3BE0DC2077CC13002A0AC0 /* ClientQueryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientQueryViewController.swift; sourceTree = ""; }; DC3BE0DD2077CC13002A0AC0 /* ClientRootViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientRootViewController.swift; sourceTree = ""; }; DC3BE0E02077CD4B002A0AC0 /* Synchronized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Synchronized.swift; sourceTree = ""; }; + DC3DDF02287E1AC200E5586D /* UIViewController+HostBundleID.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+HostBundleID.h"; sourceTree = ""; }; + DC3DDF03287E1AC200E5586D /* UIViewController+HostBundleID.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+HostBundleID.m"; sourceTree = ""; }; DC3DEC7A22AFA1F000F3352D /* DownloadItemsHUDViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadItemsHUDViewController.swift; sourceTree = ""; }; DC3DEC7C22AFFE8E00F3352D /* KVOWaiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KVOWaiter.swift; sourceTree = ""; }; DC3DEC7F22B03AE700F3352D /* CardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViewController.swift; sourceTree = ""; }; @@ -2329,6 +2333,15 @@ path = Client; sourceTree = ""; }; + DC3DDEFE287E1AA500E5586D /* UIKit Extensions */ = { + isa = PBXGroup; + children = ( + DC3DDF03287E1AC200E5586D /* UIViewController+HostBundleID.m */, + DC3DDF02287E1AC200E5586D /* UIViewController+HostBundleID.h */, + ); + path = "UIKit Extensions"; + sourceTree = ""; + }; DC422448207CAED60006A2A6 /* Theming */ = { isa = PBXGroup; children = ( @@ -2651,6 +2664,7 @@ DCC832E5242CB14E00153F8C /* Notifications */, DC774E5422F44DF6000B11A1 /* SDK Extensions */, DC0030BE2350B1CE00BB8570 /* Tools */, + DC3DDEFE287E1AA500E5586D /* UIKit Extensions */, DC774E5B22F44E4A000B11A1 /* ZIP Archive */, DC774E6522F44EA7000B11A1 /* Resources */, ); @@ -3157,6 +3171,7 @@ DCFEFE2A236876BD009A142F /* OCLicenseManager.h in Headers */, DCFEFE4923687C83009A142F /* OCLicenseEntitlement.h in Headers */, DC4332002472E1B4002DC0E5 /* OCLicenseEMMProvider.h in Headers */, + DC3DDF07287E1C0E00E5586D /* UIViewController+HostBundleID.h in Headers */, DCFEFE39236877A7009A142F /* OCLicenseFeature.h in Headers */, DC23D1DA238F391200423F62 /* OCLicenseAppStoreReceipt.h in Headers */, DC70398526128B89009F2DC1 /* NSString+ByteCountParser.h in Headers */, @@ -4292,6 +4307,7 @@ DCFEFE50236880B5009A142F /* OCLicenseOffer.m in Sources */, DC0030C12350B1CE00BB8570 /* NSData+Encoding.m in Sources */, DC774E5F22F44E57000B11A1 /* ZIPArchive.m in Sources */, + DC3DDF06287E1C0800E5586D /* UIViewController+HostBundleID.m in Sources */, DCDBB60B2525306000FAD707 /* NotificationAuthErrorForwarder.m in Sources */, DCD71E8027427463001592C6 /* BuildOptions.m in Sources */, DC080CE5238AE3F40044C5D2 /* OCLicenseAppStoreProvider.m in Sources */, @@ -4670,8 +4686,8 @@ APP_BUILD_FLAGS = "$(inherited)"; APP_BUILD_FLAGS_SWIFT = "$(APP_BUILD_FLAGS)"; APP_PRODUCT_NAME = ownCloud; - APP_SHORT_VERSION = 11.10.0; - APP_VERSION = 217; + APP_SHORT_VERSION = 11.10.1; + APP_VERSION = 225; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -4739,8 +4755,8 @@ APP_BUILD_FLAGS = "$(inherited)"; APP_BUILD_FLAGS_SWIFT = "$(APP_BUILD_FLAGS)"; APP_PRODUCT_NAME = ownCloud; - APP_SHORT_VERSION = 11.10.0; - APP_VERSION = 217; + APP_SHORT_VERSION = 11.10.1; + APP_VERSION = 225; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; diff --git a/ownCloud/Release Notes/ReleaseNotes.plist b/ownCloud/Release Notes/ReleaseNotes.plist index 1b56de0f7..8b5391988 100644 --- a/ownCloud/Release Notes/ReleaseNotes.plist +++ b/ownCloud/Release Notes/ReleaseNotes.plist @@ -1613,6 +1613,23 @@ Added an optional "Wait for completion" option to the "Save File& + + Version + 11.10.1 + ReleaseNotes + + + Title + Available Offline Folders + Subtitle + Shows the contents of the available folders when offline. + Type + Fix + ImageName + wrench + + + diff --git a/ownCloud/Static Login/Interface/StaticLoginSetupViewController.swift b/ownCloud/Static Login/Interface/StaticLoginSetupViewController.swift index ec3f65406..359a1845f 100644 --- a/ownCloud/Static Login/Interface/StaticLoginSetupViewController.swift +++ b/ownCloud/Static Login/Interface/StaticLoginSetupViewController.swift @@ -118,7 +118,7 @@ class StaticLoginSetupViewController : StaticLoginStepViewController { onboardingSection = StaticTableViewSection(headerTitle: nil, identifier: "onboardingSection") if let message = profile.promptForHelpURL, let title = profile.helpURLButtonString { - let (proceedButton, _) = onboardingSection.addButtonFooter(message: message, messageItemStyle: .welcomeMessage, proceedLabel: title, proceedItemStyle: .informal, cancelLabel: nil) + let (proceedButton, _) = onboardingSection.addButtonFooter(message: message, messageItemStyle: .welcomeMessage, proceedLabel: title, proceedItemStyle: .welcomeInformal, cancelLabel: nil) proceedButton?.addTarget(self, action: #selector(self.helpAction), for: .touchUpInside) } diff --git a/ownCloudAppFramework/AppLock Settings/AppLockSettings.h b/ownCloudAppFramework/AppLock Settings/AppLockSettings.h index 13fc01a6c..941153565 100644 --- a/ownCloudAppFramework/AppLock Settings/AppLockSettings.h +++ b/ownCloudAppFramework/AppLock Settings/AppLockSettings.h @@ -31,6 +31,8 @@ NS_ASSUME_NONNULL_BEGIN @property(assign,nonatomic) BOOL lockEnabled; @property(assign,nonatomic) NSInteger lockDelay; @property(assign,nonatomic) BOOL biometricalSecurityEnabled; +@property(assign,nonatomic) BOOL biometricalSecurityEnabledinShareSheet; +@property(readonly,nonatomic,nullable) NSURL *biometricalAuthenticationRedirectionTargetURL; @property(readonly,nonatomic) BOOL isPasscodeEnforced; @property(readonly,nonatomic) NSInteger requiredPasscodeDigits; @@ -46,5 +48,6 @@ extern OCClassSettingsKey OCClassSettingsKeyRequiredPasscodeDigits; extern OCClassSettingsKey OCClassSettingsKeyMaximumPasscodeDigits; extern OCClassSettingsKey OCClassSettingsKeyPasscodeLockDelay; extern OCClassSettingsKey OCClassSettingsKeyPasscodeUseBiometricalUnlock; +extern OCClassSettingsKey OCClassSettingsKeyPasscodeShareSheetBiometricalUnlockByApp; NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/AppLock Settings/AppLockSettings.m b/ownCloudAppFramework/AppLock Settings/AppLockSettings.m index 46622d520..25c57542d 100644 --- a/ownCloudAppFramework/AppLock Settings/AppLockSettings.m +++ b/ownCloudAppFramework/AppLock Settings/AppLockSettings.m @@ -17,6 +17,7 @@ */ #import "AppLockSettings.h" +#import "Branding.h" @implementation AppLockSettings @@ -54,7 +55,19 @@ + (OCClassSettingsIdentifier)classSettingsIdentifier OCClassSettingsKeyPasscodeEnforced : @(NO), OCClassSettingsKeyRequiredPasscodeDigits : @(4), OCClassSettingsKeyMaximumPasscodeDigits : @(6), - OCClassSettingsKeyPasscodeUseBiometricalUnlock : @(NO) + OCClassSettingsKeyPasscodeUseBiometricalUnlock : @(NO), + OCClassSettingsKeyPasscodeShareSheetBiometricalUnlockByApp : @{ + @"default" : @{ + @"allow" : @(YES) + }, + + // For unknown reasons invoking biometric authentication from the + // share sheet in Boxer leads to dismissal of the entire share sheet, + // so (as of July 2022) we hardcode it as an exception here + @"com.air-watch.boxer" : @{ + @"allow" : @(NO) + } + } }); } @@ -89,9 +102,16 @@ + (OCClassSettingsMetadataCollection)classSettingsMetadata OCClassSettingsMetadataKeyCategory : @"Passcode" }, - OCClassSettingsKeyPasscodeUseBiometricalUnlock : @{ - OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeBoolean, - OCClassSettingsMetadataKeyDescription : @"Controls wether the biometrical unlock will be enabled automatically.", + OCClassSettingsKeyPasscodeUseBiometricalUnlock : @{ + OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeBoolean, + OCClassSettingsMetadataKeyDescription : @"Controls wether the biometrical unlock will be enabled automatically.", + OCClassSettingsMetadataKeyStatus : OCClassSettingsKeyStatusAdvanced, + OCClassSettingsMetadataKeyCategory : @"Passcode" + }, + + OCClassSettingsKeyPasscodeShareSheetBiometricalUnlockByApp : @{ + OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeDictionary, + OCClassSettingsMetadataKeyDescription : @"Controls the biometrical unlock availability in the share sheet, with per-app level control.", OCClassSettingsMetadataKeyStatus : OCClassSettingsKeyStatusAdvanced, OCClassSettingsMetadataKeyCategory : @"Passcode" } @@ -128,14 +148,28 @@ - (void)setLockDelay:(NSInteger)lockDelay - (BOOL)biometricalSecurityEnabled { - NSNumber *useBiometricalUnlock; + NSNumber *useBiometricalUnlockNumber; + BOOL useBiometricalUnlock = NO; - if ((useBiometricalUnlock = [_userDefaults objectForKey:@"security-settings-use-biometrical"]) != nil) + if ((useBiometricalUnlockNumber = [_userDefaults objectForKey:@"security-settings-use-biometrical"]) != nil) { - return (useBiometricalUnlock.boolValue); + useBiometricalUnlock = useBiometricalUnlockNumber.boolValue; + } + else + { + useBiometricalUnlock = [[self classSettingForOCClassSettingsKey:OCClassSettingsKeyPasscodeUseBiometricalUnlock] boolValue]; } - return ([[self classSettingForOCClassSettingsKey:OCClassSettingsKeyPasscodeUseBiometricalUnlock] boolValue]); + if (useBiometricalUnlock) + { + // Apple share extension specific settings + if ([OCAppIdentity.sharedAppIdentity.componentIdentifier isEqual:OCAppComponentIdentifierShareExtension]) + { + return ([self biometricalSecurityEnabledinShareSheet]); + } + } + + return (useBiometricalUnlock); } - (void)setBiometricalSecurityEnabled:(BOOL)biometricalSecurityEnabled @@ -143,6 +177,102 @@ - (void)setBiometricalSecurityEnabled:(BOOL)biometricalSecurityEnabled [_userDefaults setBool:biometricalSecurityEnabled forKey:@"security-settings-use-biometrical"]; } +- (NSDictionary *)_shareSheetBiometricalAttributesForApp:(NSString *)hostAppID +{ + NSDictionary *shareSheetBiometricalUnlockByApp = [self classSettingForOCClassSettingsKey:OCClassSettingsKeyPasscodeShareSheetBiometricalUnlockByApp]; + NSDictionary *attributesForApp = nil; + + if ([shareSheetBiometricalUnlockByApp isKindOfClass:NSDictionary.class]) + { + if (shareSheetBiometricalUnlockByApp[hostAppID] != nil) + { + attributesForApp = OCTypedCast(shareSheetBiometricalUnlockByApp[hostAppID], NSDictionary); + } + else + { + attributesForApp = OCTypedCast(shareSheetBiometricalUnlockByApp[@"default"], NSDictionary); + } + } + + return (attributesForApp); +} + +- (NSDictionary *)_shareSheetBiometricalAttributes +{ + NSString *hostAppID; + + if ((hostAppID = OCAppIdentity.sharedAppIdentity.hostAppBundleIdentifier) == nil) + { + hostAppID = @"default"; + } + + return ([self _shareSheetBiometricalAttributesForApp:hostAppID]); +} + +- (BOOL)biometricalSecurityEnabledinShareSheet +{ + NSNumber *useBiometricalUnlock; + + if ((useBiometricalUnlock = [_userDefaults objectForKey:@"security-settings-use-biometrical-share-sheet"]) != nil) + { + return (useBiometricalUnlock.boolValue); + } + + NSDictionary *shareSheetAttributesForApp = nil; + + if ((shareSheetAttributesForApp = [self _shareSheetBiometricalAttributes]) != nil) + { + NSNumber *enabled; + + if ((enabled = OCTypedCast(shareSheetAttributesForApp[@"allow"], NSNumber)) != nil) + { + return (enabled.boolValue); + } + } + + return (YES); +} + +- (void)setBiometricalSecurityEnabledinShareSheet:(BOOL)biometricalSecurityEnabledinShareSheet +{ + [_userDefaults setBool:biometricalSecurityEnabledinShareSheet forKey:@"security-settings-use-biometrical-share-sheet"]; +} + +- (NSURL *)biometricalAuthenticationRedirectionTargetURL +{ + if ([OCAppIdentity.sharedAppIdentity.componentIdentifier isEqual:OCAppComponentIdentifierShareExtension]) + { + // Only in share extension + NSDictionary *shareSheetAttributesForApp = nil; + + if ((shareSheetAttributesForApp = [self _shareSheetBiometricalAttributes]) != nil) + { + NSString *trampolineURLString; + + // For apps with a trampoline URL, determine the target URL to initiate the authentication trampoline + if ((trampolineURLString = shareSheetAttributesForApp[@"trampoline-url"]) != nil) + { + NSString *toAppURLScheme; + + if ((toAppURLScheme = [Branding.sharedBranding appURLSchemesForBundleURLName:nil].firstObject) != nil) + { + NSString *targetURLString = [NSString stringWithFormat:@"%@://?authenticateForApp=%@", toAppURLScheme, OCAppIdentity.sharedAppIdentity.hostAppBundleIdentifier]; + + return ([NSURL URLWithString:targetURLString]); + } + } + } + } + + return (nil); +} + +// Counterpart to .biometricalAuthenticationRedirectionTargetURL for use in the app (not implemented) +//- (NSURL *)biometricalAuthenticationReturnURL +//{ +// return (nil); +//} + - (BOOL)isPasscodeEnforced { NSNumber *isPasscodeEnforced = [self classSettingForOCClassSettingsKey:OCClassSettingsKeyPasscodeEnforced]; @@ -190,3 +320,4 @@ - (BOOL)lockDelayUserSettable OCClassSettingsKey OCClassSettingsKeyMaximumPasscodeDigits = @"maximumPasscodeDigits"; OCClassSettingsKey OCClassSettingsKeyPasscodeLockDelay = @"lockDelay"; OCClassSettingsKey OCClassSettingsKeyPasscodeUseBiometricalUnlock = @"use-biometrical-unlock"; +OCClassSettingsKey OCClassSettingsKeyPasscodeShareSheetBiometricalUnlockByApp = @"share-sheet-biometrical-unlock-by-app"; diff --git a/ownCloudAppFramework/Branding/Branding.h b/ownCloudAppFramework/Branding/Branding.h index 20ddc801f..8c7a00faa 100644 --- a/ownCloudAppFramework/Branding/Branding.h +++ b/ownCloudAppFramework/Branding/Branding.h @@ -44,6 +44,8 @@ typedef NSString* BrandingImageName NS_TYPED_EXTENSIBLE_ENUM; @property(strong,nullable,nonatomic,readonly) NSBundle *appBundle; //!< Bundle of the main app +- (NSArray *)appURLSchemesForBundleURLName:(nullable NSString *)bundleURLName; //!< URL schemes from the app's Info.plist matching the provided CFBundleURLName. + @property(strong) NSDictionary *legacyKeyPathsByClassSettingsKeys; - (void)registerLegacyKeyPath:(BrandingLegacyKeyPath)keyPath forClassSettingsKey:(OCClassSettingsKey)classSettingsKey; diff --git a/ownCloudAppFramework/Branding/Branding.m b/ownCloudAppFramework/Branding/Branding.m index c66bcdb37..03e2268ce 100644 --- a/ownCloudAppFramework/Branding/Branding.m +++ b/ownCloudAppFramework/Branding/Branding.m @@ -260,6 +260,35 @@ - (NSDictionary *)userDefaultsDefaultValues return ([self computedValueForClassSettingsKey:BrandingKeyUserDefaultsDefaultValues]); } +- (NSArray *)appURLSchemesForBundleURLName:(nullable NSString *)bundleURLName +{ + NSBundle *appBundle; + NSMutableArray *appURLSchemes = [NSMutableArray new]; + + if ((appBundle = self.appBundle) != nil) + { + NSArray *urlSchemeDictionaries; + + if ((urlSchemeDictionaries = [appBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]) != nil) + { + for (NSDictionary *urlSchemesDict in urlSchemeDictionaries) + { + if ((bundleURLName == nil) || [bundleURLName isEqual:urlSchemesDict[@"CFBundleURLName"]]) + { + NSArray *urlSchemes; + + if ((urlSchemes = urlSchemesDict[@"CFBundleURLSchemes"]) != nil) + { + [appURLSchemes addObjectsFromArray:urlSchemes]; + } + } + } + } + } + + return (appURLSchemes); +} + - (NSArray *)disabledImportMethods { return ([self computedValueForClassSettingsKey:BrandingKeyDisabledImportMethods]); diff --git a/ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.h b/ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.h new file mode 100644 index 000000000..5b081780c --- /dev/null +++ b/ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.h @@ -0,0 +1,29 @@ +// +// UIViewController+HostBundleID.h +// ownCloud +// +// Created by Felix Schwarz on 12.07.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIViewController (HostBundleID) + +@property(nullable,readonly) NSString *oc_hostAppBundleIdentifier; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.m b/ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.m new file mode 100644 index 000000000..7a8711c95 --- /dev/null +++ b/ownCloudAppFramework/UIKit Extensions/UIViewController+HostBundleID.m @@ -0,0 +1,34 @@ +// +// UIViewController+HostBundleID.m +// ownCloud +// +// Created by Felix Schwarz on 12.07.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "UIViewController+HostBundleID.h" +#import + +@implementation UIViewController (HostBundleID) + +- (NSString *)oc_hostAppBundleIdentifier +{ + @try { + return ([self valueForKey:@"_hostBundleID"]); + } @catch (NSException *exception) { + } + + return (nil); +} + +@end diff --git a/ownCloudAppFramework/ownCloudApp.h b/ownCloudAppFramework/ownCloudApp.h index 6ca94e137..2f90e6990 100644 --- a/ownCloudAppFramework/ownCloudApp.h +++ b/ownCloudAppFramework/ownCloudApp.h @@ -36,6 +36,8 @@ FOUNDATION_EXPORT const unsigned char ownCloudAppVersionString[]; #import #import +#import + #import #import #import diff --git a/ownCloudAppShared/AppLock/AppLockManager.swift b/ownCloudAppShared/AppLock/AppLockManager.swift index e35c11d16..4e4b169f8 100644 --- a/ownCloudAppShared/AppLock/AppLockManager.swift +++ b/ownCloudAppShared/AppLock/AppLockManager.swift @@ -37,8 +37,16 @@ public class AppLockManager: NSObject { // MARK: - State private var lastApplicationBackgroundedDate : Date? { - didSet { - if let date = lastApplicationBackgroundedDate { + get { + if let archivedData = self.keychain?.readDataFromKeychainItem(forAccount: keychainAccount, path: keychainLockedDate) { + guard let value = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSDate.self, from: archivedData) else { return nil } + return value as? Date + } + + return nil + } + set(newValue) { + if let date = newValue { let archivedData = try? NSKeyedArchiver.archivedData(withRootObject: date as NSDate, requiringSecureCoding: true) self.keychain?.write(archivedData, toKeychainItemForAccount: keychainAccount, path: keychainLockedDate) } else { @@ -47,9 +55,17 @@ public class AppLockManager: NSObject { } } - public var unlocked: Bool = false { - didSet { - let archivedData = try? NSKeyedArchiver.archivedData(withRootObject: unlocked as NSNumber, requiringSecureCoding: true) + public var unlocked: Bool { + get { + if let archivedData = self.keychain?.readDataFromKeychainItem(forAccount: keychainAccount, path: keychainUnlocked) { + guard let value = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSNumber.self, from: archivedData)?.boolValue else { return false} + return value ?? false + } + + return false + } + set(newValue) { + let archivedData = try? NSKeyedArchiver.archivedData(withRootObject: newValue as NSNumber, requiringSecureCoding: true) self.keychain?.write(archivedData, toKeychainItemForAccount: keychainAccount, path: keychainUnlocked) } } @@ -124,6 +140,10 @@ public class AppLockManager: NSObject { // Set a view controller only, if you want to use it in an extension, when UIWindow is not working public var passwordViewHostViewController: UIViewController? + private var biometricalSecurityEnabled: Bool { + return AppLockSettings.shared.biometricalSecurityEnabled + } + // MARK: - Init public static var shared = AppLockManager() @@ -148,7 +168,7 @@ public class AppLockManager: NSObject { } // MARK: - Show / Dismiss Passcode View - public func showLockscreenIfNeeded(forceShow: Bool = false, setupMode: Bool = false, context: LAContext = LAContext()) { + public func showLockscreenIfNeeded(forceShow: Bool = false, setupMode: Bool = false, context: LAContext? = nil) { if self.shouldDisplayLockscreen || forceShow || setupMode { lockscreenOpenForced = forceShow lockscreenOpen = true @@ -159,6 +179,8 @@ public class AppLockManager: NSObject { } else if setupMode { showBiometricalAuthenticationInterface(context: context) } + } else { + dismissLockscreen(animated: true) } } @@ -296,15 +318,14 @@ public class AppLockManager: NSObject { passcodeViewController = PasscodeViewController(biometricalHandler: { (passcodeViewController) in if !self.shouldDisplayCountdown { - let context = LAContext() - self.showBiometricalAuthenticationInterface(context: context) + self.showBiometricalAuthenticationInterface() } }, completionHandler: { (viewController: PasscodeViewController, passcode: String) in self.attemptUnlock(with: passcode, passcodeViewController: viewController) }, requiredLength: AppLockManager.shared.passcode?.count ?? AppLockSettings.shared.requiredPasscodeDigits) passcodeViewController.message = "Enter code".localized - passcodeViewController.cancelButtonHidden = false + passcodeViewController.cancelButtonAvailable = false passcodeViewController.screenBlurringEnabled = lockscreenOpenForced && !self.shouldDisplayLockscreen @@ -313,14 +334,19 @@ public class AppLockManager: NSObject { // MARK: - App Events @objc func appDidEnterBackground() { - lastApplicationBackgroundedDate = Date() + if unlocked { + lastApplicationBackgroundedDate = Date() + } else { + lastApplicationBackgroundedDate = nil + } showLockscreenIfNeeded(forceShow: true) } @objc func appWillEnterForeground() { if self.shouldDisplayLockscreen { - showLockscreenIfNeeded() + dismissLockscreen(animated: false) + self.showLockscreenIfNeeded() } else { dismissLockscreen(animated: false) } @@ -330,11 +356,11 @@ public class AppLockManager: NSObject { func attemptUnlock(with testPasscode: String?, customErrorMessage: String? = nil, passcodeViewController: PasscodeViewController? = nil) { if testPasscode == self.passcode { unlocked = true - lastApplicationBackgroundedDate = nil failedPasscodeAttempts = 0 dismissLockscreen(animated: true) } else { unlocked = false + lastApplicationBackgroundedDate = nil passcodeViewController?.errorMessage = (customErrorMessage != nil) ? customErrorMessage! : "Incorrect code".localized failedPasscodeAttempts += 1 @@ -455,8 +481,20 @@ public class AppLockManager: NSObject { // MARK: - Biometrical Unlock private var biometricalAuthenticationInterfaceShown : Bool = false - func showBiometricalAuthenticationInterface(context: LAContext) { - if shouldDisplayLockscreen, AppLockSettings.shared.biometricalSecurityEnabled, !biometricalAuthenticationInterfaceShown { + func showBiometricalAuthenticationInterface(context inContext: LAContext? = nil) { + + if shouldDisplayLockscreen, biometricalSecurityEnabled, !biometricalAuthenticationInterfaceShown { + // Check if we should perform biometrical authentication - or redirect + if let targetURL = AppLockSettings.shared.biometricalAuthenticationRedirectionTargetURL { + // Unfortunately, opening the URL closes the share sheet just like invoking + // biometric auth - so in those instances where we'd want to use it to work around + // that. + self.passwordViewHostViewController?.openURL(targetURL) + return + } + + // Perform biometrical authentication + let context = inContext ?? LAContext() var evaluationError: NSError? // Check if the device can evaluate the policy. @@ -517,7 +555,7 @@ public class AppLockManager: NSObject { } } } else { - if let error = evaluationError, AppLockSettings.shared.biometricalSecurityEnabled { + if let error = evaluationError, biometricalSecurityEnabled { OnMainThread { self.performPasscodeViewControllerUpdates { (passcodeViewController) in passcodeViewController.errorMessage = error.localizedDescription diff --git a/ownCloudAppShared/AppLock/PasscodeViewController.swift b/ownCloudAppShared/AppLock/PasscodeViewController.swift index fe5157afa..1473f4541 100644 --- a/ownCloudAppShared/AppLock/PasscodeViewController.swift +++ b/ownCloudAppShared/AppLock/PasscodeViewController.swift @@ -107,18 +107,18 @@ public class PasscodeViewController: UIViewController, Themeable { } } - var cancelButtonHidden: Bool { + var cancelButtonAvailable: Bool { didSet { - cancelButton?.isEnabled = cancelButtonHidden - cancelButton?.isHidden = !cancelButtonHidden + cancelButton?.isEnabled = cancelButtonAvailable + cancelButton?.isHidden = !cancelButtonAvailable } } var biometricalButtonHidden: Bool = false { didSet { - biometricalButton?.isEnabled = biometricalButtonHidden - biometricalButton?.isHidden = !biometricalButtonHidden - biometricalImageView?.isHidden = !biometricalButtonHidden + biometricalButton?.isEnabled = !biometricalButtonHidden + biometricalButton?.isHidden = biometricalButtonHidden + biometricalImageView?.isHidden = biometricalButtonHidden biometricalImageView?.image = LAContext().biometricsAuthenticationImage() } } @@ -142,7 +142,7 @@ public class PasscodeViewController: UIViewController, Themeable { self.biometricalHandler = biometricalHandler self.completionHandler = completionHandler self.keypadButtonsEnabled = keypadButtonsEnabled - self.cancelButtonHidden = hasCancelButton + self.cancelButtonAvailable = hasCancelButton self.keypadButtonsHidden = false self.screenBlurringEnabled = false self.passcodeLength = requiredLength @@ -167,13 +167,13 @@ public class PasscodeViewController: UIViewController, Themeable { self.errorMessage = { self.errorMessage }() self.timeoutMessage = { self.timeoutMessage }() - self.cancelButtonHidden = { self.cancelButtonHidden }() + self.cancelButtonAvailable = { self.cancelButtonAvailable }() self.keypadButtonsEnabled = { self.keypadButtonsEnabled }() self.keypadButtonsHidden = { self.keypadButtonsHidden }() self.screenBlurringEnabled = { self.screenBlurringEnabled }() self.errorMessageLabel?.minimumScaleFactor = 0.5 self.errorMessageLabel?.adjustsFontSizeToFitWidth = true - self.biometricalButtonHidden = !((!AppLockSettings.shared.biometricalSecurityEnabled || !AppLockSettings.shared.lockEnabled) || self.cancelButtonHidden) + self.biometricalButtonHidden = (!AppLockSettings.shared.biometricalSecurityEnabled || !AppLockSettings.shared.lockEnabled || cancelButtonAvailable) // cancelButtonAvailable is true for setup tasks/settings changes only updateKeypadButtons() if let biometricalSecurityName = LAContext().supportedBiometricsAuthenticationName() { self.biometricalButton?.accessibilityLabel = biometricalSecurityName diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index 430793be9..28463c2d5 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -31,6 +31,17 @@ public extension OCQueryState { } } +public extension OCCoreConnectionStatus { + var isOffline: Bool { + switch self { + case .offline, .unavailable: + return true + default: + return false + } + } +} + public protocol MultiSelectSupport { func setupMultiselection() func enterMultiselection() @@ -296,32 +307,37 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa self.actionContext = ActionContext(viewController: self, core: core, query: query, items: [OCItem](), location: actionsLocation) } - switch query.state { - case .contentsFromCache, .idle, .waitingForServerReply: - if previousItemCount == 0, self.items.count == 0, query.state == .waitingForServerReply { - break - } - - if query.state.isFinal { - if self.items.count == 0 { - if self.searchController?.searchBar.text != "" { - self.messageView?.message(show: true, with: UIEdgeInsets(top: sortBar?.frame.size.height ?? 0, left: 0, bottom: 0, right: 0), imageName: "icon-search", title: "No matches".localized, message: "There is no results for this search".localized) - } else { - self.messageView?.message(show: true, imageName: "folder", title: "Empty folder".localized, message: "This folder contains no files or folders.".localized) - } - } else { - self.messageView?.message(show: false) - } - - self.reloadTableData() - } - case .targetRemoved: - self.messageView?.message(show: true, imageName: "folder", title: "Folder removed".localized, message: "This folder no longer exists on the server.".localized) - self.reloadTableData() - - default: - self.messageView?.message(show: false) - } + switch query.state { + case .contentsFromCache, .idle, .waitingForServerReply: + let latestItemCount = self.items.count + + if previousItemCount == 0, latestItemCount == 0, query.state == .waitingForServerReply { + break + } + + // Refresh on: + if (previousItemCount != latestItemCount) || // - item count change + query.state.isFinal || // - query state is final (== anything but .waitingForServerReply and .started) + ((query.state == .waitingForServerReply) && (core?.connectionStatus.isOffline == true)) { // - when waiting for a server reply while the connection is offline + if latestItemCount == 0 { + if self.searchController?.searchBar.text != "" { + self.messageView?.message(show: true, with: UIEdgeInsets(top: sortBar?.frame.size.height ?? 0, left: 0, bottom: 0, right: 0), imageName: "icon-search", title: "No matches".localized, message: "There is no results for this search".localized) + } else { + self.messageView?.message(show: true, imageName: "folder", title: "Empty folder".localized, message: "This folder contains no files or folders.".localized) + } + } else { + self.messageView?.message(show: false) + } + + self.reloadTableData() + } + case .targetRemoved: + self.messageView?.message(show: true, imageName: "folder", title: "Folder removed".localized, message: "This folder no longer exists on the server.".localized) + self.reloadTableData() + + default: + self.messageView?.message(show: false) + } } // MARK: - Themeable diff --git a/ownCloudAppShared/UIKit Extension/UIColor+Extension.swift b/ownCloudAppShared/UIKit Extension/UIColor+Extension.swift index 53c2d4a2c..069d312f4 100644 --- a/ownCloudAppShared/UIKit Extension/UIColor+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/UIColor+Extension.swift @@ -100,4 +100,10 @@ extension UIColor { return (String(format: "\(leadIn)%02x%02x%02x", Int(selfRed*255.0), Int(selfGreen*255.0), Int(selfBlue*255.0))) } + + public func isLight() -> Bool { + guard let components = cgColor.components, components.count > 2 else {return false} + let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 + return (brightness > 0.5) + } } diff --git a/ownCloudAppShared/UIKit Extension/UIViewController+Extension.swift b/ownCloudAppShared/UIKit Extension/UIViewController+Extension.swift index e024d90ce..a685ff4e9 100644 --- a/ownCloudAppShared/UIKit Extension/UIViewController+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/UIViewController+Extension.swift @@ -55,4 +55,15 @@ public extension UIViewController { return self } + + @objc @discardableResult func openURL(_ url: URL) -> Bool { + var responder: UIResponder? = self.navigationController + while responder != nil { + if let application = responder as? UIApplication { + return application.perform(#selector(openURL(_:)), with: url) != nil + } + responder = responder?.next + } + return true + } } diff --git a/ownCloudAppShared/User Interface/Theme/NSObject+ThemeApplication.swift b/ownCloudAppShared/User Interface/Theme/NSObject+ThemeApplication.swift index e2deb87c7..a81f8099f 100644 --- a/ownCloudAppShared/User Interface/Theme/NSObject+ThemeApplication.swift +++ b/ownCloudAppShared/User Interface/Theme/NSObject+ThemeApplication.swift @@ -43,6 +43,7 @@ public enum ThemeItemStyle { case purchase case welcome + case welcomeInformal } public enum ThemeItemState { @@ -86,9 +87,14 @@ public extension NSObject { case .purchase: themeButton.themeColorCollection = collection.purchaseColors - - case .welcome: - themeButton.themeColorCollection = collection.loginColors.filledColorPairCollection + + case .welcome: + themeButton.themeColorCollection = collection.loginColors.filledColorPairCollection + + case .welcomeInformal: + let fromPair = collection.loginColors.filledColorPairCollection + let normal = ThemeColorPair(foreground: fromPair.normal.foreground.lighter(0.25), background: fromPair.normal.background.lighter(0.25)) + themeButton.themeColorCollection = ThemeColorPairCollection(fromPair: normal) case .informal: themeButton.themeColorCollection = collection.informalColors.filledColorPairCollection @@ -210,6 +216,7 @@ public extension NSObject { case .welcomeMessage: normalColor = collection.loginColors.secondaryLabelColor + normalColor = collection.loginColors.secondaryLabelColor highlightColor = collection.loginColors.secondaryLabelColor case .message, .bigMessage: diff --git a/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift b/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift index 653f6b256..cd1e87478 100644 --- a/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift +++ b/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift @@ -397,7 +397,16 @@ public class ThemeCollection : NSObject { self.loginColors = colors.resolveThemeColorCollection("Login", self.darkBrandColors) // Bar styles - self.statusBarStyle = styleResolver.resolveStatusBarStyle(for: "statusBarStyle", fallback: .lightContent) + var defaultStatusBarStyle : UIStatusBarStyle = .lightContent + if let backgroundColor = self.navigationBarColors.backgroundColor, backgroundColor.isLight() { + if #available(iOSApplicationExtension 13.0, *) { + defaultStatusBarStyle = .darkContent + } else { + defaultStatusBarStyle = .default + } + } + + self.statusBarStyle = styleResolver.resolveStatusBarStyle(for: "statusBarStyle", fallback: defaultStatusBarStyle) self.loginStatusBarStyle = styleResolver.resolveStatusBarStyle(for: "loginStatusBarStyle", fallback: self.statusBarStyle) self.barStyle = styleResolver.resolveBarStyle(fallback: .black) @@ -411,10 +420,10 @@ public class ThemeCollection : NSObject { // Logo fill color logoFillColor = UIColor.lightGray - if lightBrandColor.isEqual(UIColor(hex: 0xFFFFFF)) { + if lightBrandColor.isLight() { self.neutralColors.normal.background = self.darkBrandColor self.lightBrandColors.filledColorPairCollection.normal.background = self.darkBrandColor - } + } } self.informalColors = colors.resolveThemeColorCollection("Informal", self.lightBrandColors)