diff --git a/ios-sdk b/ios-sdk index ee40e1925..747d90e45 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit ee40e1925bdf488f6cd7b2c8468f357b629f529f +Subproject commit 747d90e45c9aadb4c6700d49508beafd2b14f500 diff --git a/ownCloud File Provider/Info.plist b/ownCloud File Provider/Info.plist index 2a6fd1b1d..5c73fc0da 100644 --- a/ownCloud File Provider/Info.plist +++ b/ownCloud File Provider/Info.plist @@ -39,5 +39,10 @@ OCKeychainAccessGroupIdentifier group.com.owncloud.ios-app + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index b9a4c0406..e66c8ff11 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -66,6 +66,7 @@ 39E98B3E22797D1B009911F1 /* PublicLinkTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */; }; 39E98B452279ACF5009911F1 /* PublicLinkEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E98B442279ACF5009911F1 /* PublicLinkEditTableViewController.swift */; }; 46B9D336BF7FE50321823888 /* Pods_ownCloudScreenshotsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54199937F74A129BC74DEB0A /* Pods_ownCloudScreenshotsTests.framework */; }; + 4C11EE5B22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C11EE5A22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift */; }; 4C1561E8222321E0009C4EF3 /* PhotoSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1561E7222321E0009C4EF3 /* PhotoSelectionViewController.swift */; }; 4C1561EF22232357009C4EF3 /* PhotoSelectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1561EE22232357009C4EF3 /* PhotoSelectionViewCell.swift */; }; 4C16CBA7226F0F1A00D67BB6 /* FileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C16CBA6226F0F1900D67BB6 /* FileTests.swift */; }; @@ -78,13 +79,23 @@ 4C464BF42187AF1500D30602 /* PDFSearchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BEC2187AF1500D30602 /* PDFSearchTableViewCell.swift */; }; 4C464BF52187AF1500D30602 /* PDFThumbnailsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BED2187AF1500D30602 /* PDFThumbnailsCollectionViewController.swift */; }; 4C464BF62187AF1500D30602 /* PDFTocItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C464BEE2187AF1500D30602 /* PDFTocItem.swift */; }; + 4C51727D22DE04BD001BC97F /* ScheduledTaskExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C51727522DE04BD001BC97F /* ScheduledTaskExtension.swift */; }; + 4C51727E22DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C51727622DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift */; }; + 4C51727F22DE04BD001BC97F /* ScheduledTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C51727722DE04BD001BC97F /* ScheduledTaskManager.swift */; }; 4C6B78102226B83300C5F3DB /* PhotoAlbumTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6B780F2226B83300C5F3DB /* PhotoAlbumTableViewController.swift */; }; 4C6B78122226B86300C5F3DB /* PhotoAlbumTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6B78112226B86300C5F3DB /* PhotoAlbumTableViewCell.swift */; }; 4C7295D8228C384E00FA4E68 /* LogFilesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7295D7228C384E00FA4E68 /* LogFilesViewController.swift */; }; 4C82D07022C9387300835F0B /* MediaDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C82D06F22C9387300835F0B /* MediaDisplayViewController.swift */; }; 4C88041822E78D790016CBA9 /* MediaFilesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C88041722E78D790016CBA9 /* MediaFilesSettings.swift */; }; 4CAF783C2282FD40000C85CF /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAF783B2282FD40000C85CF /* FileManager+Extension.swift */; }; + 4CB8ADDE22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8ADDD22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift */; }; + 4CB8ADE022DF5EC500F1FEBC /* UIAlertViewController+SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8ADDF22DF5EC500F1FEBC /* UIAlertViewController+SystemPermissions.swift */; }; + 4CB8ADE322DF6BA700F1FEBC /* PHAsset+Upload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8ADE222DF6BA700F1FEBC /* PHAsset+Upload.swift */; }; + 4CB8ADE622DF6C2B00F1FEBC /* CIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8ADE522DF6C2B00F1FEBC /* CIImage+Extensions.swift */; }; + 4CB8ADE922DF6DE200F1FEBC /* AVAsset+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8ADE822DF6DE200F1FEBC /* AVAsset+Extension.swift */; }; 4CC46D212284C677009E938F /* BookmarkInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC46D202284C677009E938F /* BookmarkInfoViewController.swift */; }; + 4CC4A21222FA20AD00AE7E2C /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC4A21122FA20AD00AE7E2C /* URL+Extensions.swift */; }; + 4CC4A21922FB4F4C00AE7E2C /* MediaUploadQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC4A21822FB4F4C00AE7E2C /* MediaUploadQueue.swift */; }; 4CF8CAB121F9B70600B8CA67 /* UIBarButtonItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF8CAB021F9B70500B8CA67 /* UIBarButtonItem+Extension.swift */; }; 59056CAD22414F3C00A18A22 /* ownCloudScreenshotsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59056CAC22414F3C00A18A22 /* ownCloudScreenshotsTests.swift */; }; 59056CB422414F8000A18A22 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59371D7B224103D300C6BC5B /* SnapshotHelper.swift */; }; @@ -631,6 +642,7 @@ 39E98B442279ACF5009911F1 /* PublicLinkEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLinkEditTableViewController.swift; sourceTree = ""; }; 3D753147564B1E4F47826109 /* Pods-ownCloud Screenshots Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ownCloud Screenshots Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ownCloud Screenshots Tests/Pods-ownCloud Screenshots Tests.debug.xcconfig"; sourceTree = ""; }; 42866B2892DC9EDC65D844E7 /* Pods_ownCloud_Screenshots_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ownCloud_Screenshots_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4C11EE5A22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantMediaUploadTaskExtension.swift; sourceTree = ""; }; 4C1561E7222321E0009C4EF3 /* PhotoSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoSelectionViewController.swift; sourceTree = ""; }; 4C1561EE22232357009C4EF3 /* PhotoSelectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoSelectionViewCell.swift; sourceTree = ""; }; 4C16CBA6226F0F1900D67BB6 /* FileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTests.swift; sourceTree = ""; }; @@ -643,13 +655,23 @@ 4C464BEC2187AF1500D30602 /* PDFSearchTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFSearchTableViewCell.swift; sourceTree = ""; }; 4C464BED2187AF1500D30602 /* PDFThumbnailsCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFThumbnailsCollectionViewController.swift; sourceTree = ""; }; 4C464BEE2187AF1500D30602 /* PDFTocItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PDFTocItem.swift; sourceTree = ""; }; + 4C51727522DE04BD001BC97F /* ScheduledTaskExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduledTaskExtension.swift; sourceTree = ""; }; + 4C51727622DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundFetchUpdateTaskAction.swift; sourceTree = ""; }; + 4C51727722DE04BD001BC97F /* ScheduledTaskManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduledTaskManager.swift; sourceTree = ""; }; 4C6B780F2226B83300C5F3DB /* PhotoAlbumTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoAlbumTableViewController.swift; sourceTree = ""; }; 4C6B78112226B86300C5F3DB /* PhotoAlbumTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoAlbumTableViewCell.swift; sourceTree = ""; }; 4C7295D7228C384E00FA4E68 /* LogFilesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogFilesViewController.swift; sourceTree = ""; }; 4C82D06F22C9387300835F0B /* MediaDisplayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDisplayViewController.swift; sourceTree = ""; }; 4C88041722E78D790016CBA9 /* MediaFilesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaFilesSettings.swift; sourceTree = ""; }; 4CAF783B2282FD40000C85CF /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = ""; }; + 4CB8ADDD22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHPhotoLibrary+Extension.swift"; sourceTree = ""; }; + 4CB8ADDF22DF5EC500F1FEBC /* UIAlertViewController+SystemPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertViewController+SystemPermissions.swift"; sourceTree = ""; }; + 4CB8ADE222DF6BA700F1FEBC /* PHAsset+Upload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHAsset+Upload.swift"; sourceTree = ""; }; + 4CB8ADE522DF6C2B00F1FEBC /* CIImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CIImage+Extensions.swift"; sourceTree = ""; }; + 4CB8ADE822DF6DE200F1FEBC /* AVAsset+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAsset+Extension.swift"; sourceTree = ""; }; 4CC46D202284C677009E938F /* BookmarkInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkInfoViewController.swift; sourceTree = ""; }; + 4CC4A21122FA20AD00AE7E2C /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; + 4CC4A21822FB4F4C00AE7E2C /* MediaUploadQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadQueue.swift; sourceTree = ""; }; 4CF8CAB021F9B70500B8CA67 /* UIBarButtonItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+Extension.swift"; sourceTree = ""; }; 54199937F74A129BC74DEB0A /* Pods_ownCloudScreenshotsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ownCloudScreenshotsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 59056CAA22414F3C00A18A22 /* ownCloudScreenshotsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ownCloudScreenshotsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1016,6 +1038,7 @@ 397328E822D6067B006CFAA4 /* Import */, DCF4F1612051925A00189B9A /* Bookmarks */, DC29F09122974F8000F77349 /* FileLists */, + 4C51727422DE04BD001BC97F /* Tasks */, DC3BE0DB2077CC13002A0AC0 /* Client */, DC7DF17C205140F400189B9A /* Server List */, DCF4F1802051A91500189B9A /* Settings */, @@ -1023,6 +1046,9 @@ DCF4F1622051927200189B9A /* UI Elements */, 239F1314205A69240029F186 /* UIKit Extensions */, DCE5E8B62080D8B8005F60CE /* SDK Extensions */, + 4CB8ADE422DF6BE300F1FEBC /* CoreImage Extensions */, + 4CB8ADE722DF6DC400F1FEBC /* AVFoundation Extensions */, + 4CB8ADD722DF5CD100F1FEBC /* PhotoKit Extensions */, DCF4F1872052BA3500189B9A /* Tools */, DC27A19B20CAB5D7008ACB6C /* FileProvider Integration */, DC85573120513C7500189B9A /* Resources */, @@ -1102,6 +1128,7 @@ DC018F8220A0F56300135198 /* UIView+Animation.swift */, 6E83C78320A33C180066EC23 /* LAContext+Extension.swift */, DC434D1220D7A8F100740056 /* UIAlertController+OCIssue.swift */, + 4CB8ADDF22DF5EC500F1FEBC /* UIAlertViewController+SystemPermissions.swift */, DC248C66213E7DB00067FE94 /* NSLayoutConstraint+Extension.swift */, 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */, 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */, @@ -1162,6 +1189,17 @@ path = Import; sourceTree = ""; }; + 4C51727422DE04BD001BC97F /* Tasks */ = { + isa = PBXGroup; + children = ( + 4C51727522DE04BD001BC97F /* ScheduledTaskExtension.swift */, + 4C51727622DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift */, + 4C51727722DE04BD001BC97F /* ScheduledTaskManager.swift */, + 4C11EE5A22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift */, + ); + path = Tasks; + sourceTree = ""; + }; 4C82D06E22C9384700835F0B /* Media */ = { isa = PBXGroup; children = ( @@ -1170,6 +1208,32 @@ path = Media; sourceTree = ""; }; + 4CB8ADD722DF5CD100F1FEBC /* PhotoKit Extensions */ = { + isa = PBXGroup; + children = ( + 4CB8ADDD22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift */, + 4CB8ADE222DF6BA700F1FEBC /* PHAsset+Upload.swift */, + 4CC4A21822FB4F4C00AE7E2C /* MediaUploadQueue.swift */, + ); + path = "PhotoKit Extensions"; + sourceTree = ""; + }; + 4CB8ADE422DF6BE300F1FEBC /* CoreImage Extensions */ = { + isa = PBXGroup; + children = ( + 4CB8ADE522DF6C2B00F1FEBC /* CIImage+Extensions.swift */, + ); + path = "CoreImage Extensions"; + sourceTree = ""; + }; + 4CB8ADE722DF6DC400F1FEBC /* AVFoundation Extensions */ = { + isa = PBXGroup; + children = ( + 4CB8ADE822DF6DE200F1FEBC /* AVAsset+Extension.swift */, + ); + path = "AVFoundation Extensions"; + sourceTree = ""; + }; 59056CAB22414F3C00A18A22 /* ownCloudScreenshotsTests */ = { isa = PBXGroup; children = ( @@ -1755,6 +1819,7 @@ DCB44D86218718BA00DAA4CC /* VendorServices.swift */, 4CAF783B2282FD40000C85CF /* FileManager+Extension.swift */, DC3DEC7C22AFFE8E00F3352D /* KVOWaiter.swift */, + 4CC4A21122FA20AD00AE7E2C /* URL+Extensions.swift */, ); path = Tools; sourceTree = ""; @@ -2401,17 +2466,21 @@ 2347446A20761BB700859C93 /* String+Extension.swift in Sources */, DCF4F17920519F8C00189B9A /* StaticTableViewController.swift in Sources */, DC680576212DF548006C3B1F /* CertificateManagementViewController.swift in Sources */, + 4CB8ADE322DF6BA700F1FEBC /* PHAsset+Upload.swift in Sources */, DC7DBA25207F684700E7337D /* ThemeResource.swift in Sources */, 4C7295D8228C384E00FA4E68 /* LogFilesViewController.swift in Sources */, DC01CDCC212EDDF600FC8E38 /* TextViewController.swift in Sources */, + 4CB8ADE622DF6C2B00F1FEBC /* CIImage+Extensions.swift in Sources */, DC33939D22E076E300DD3DA4 /* MakeUnavailableOfflineAction.swift in Sources */, 3998F5D72241486F00B66713 /* OCCertificate+Extension.swift in Sources */, 6E4F1734217749910049A71B /* ImageDisplayViewController.swift in Sources */, + 4C11EE5B22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift in Sources */, DC6CF7FB219446050013B9F9 /* LogSettingsViewController.swift in Sources */, 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */, 3998F5CC2240CD8300B66713 /* RoundedInfoView.swift in Sources */, 23EC775D2137FB6B0032D4E6 /* WebViewDisplayViewController.swift in Sources */, DC018F8C20A1060A00135198 /* ProgressHUDViewController.swift in Sources */, + 4CB8ADE922DF6DE200F1FEBC /* AVAsset+Extension.swift in Sources */, 39CC8B01228C8A950020253B /* MediaUploadSettingsSection.swift in Sources */, DC7DBA29207F71D600E7337D /* VectorImage.swift in Sources */, 4C464BF12187AF1500D30602 /* PDFTocTableViewCell.swift in Sources */, @@ -2423,6 +2492,7 @@ 39A513AC22674E56002CF1AA /* OCCore+Extension.swift in Sources */, DC018F8320A0F56300135198 /* UIView+Animation.swift in Sources */, 4CF8CAB121F9B70600B8CA67 /* UIBarButtonItem+Extension.swift in Sources */, + 4CC4A21222FA20AD00AE7E2C /* URL+Extensions.swift in Sources */, DC42244A207CAFAA0006A2A6 /* Theme.swift in Sources */, 4C464BF52187AF1500D30602 /* PDFThumbnailsCollectionViewController.swift in Sources */, 4C464BF02187AF1500D30602 /* PDFTocTableViewController.swift in Sources */, @@ -2431,6 +2501,7 @@ DC1B2708209CF0D3004715E1 /* IssuesPresentationAnimator.swift in Sources */, DC63208721FCEE5D007EC0A8 /* ProgressView.swift in Sources */, DC3BE0DF2077CC14002A0AC0 /* ClientRootViewController.swift in Sources */, + 4CB8ADE022DF5EC500F1FEBC /* UIAlertViewController+SystemPermissions.swift in Sources */, 396BE4CA2289500E00B254A9 /* RoundedLabel.swift in Sources */, DC854936218331CF00782BA8 /* UserInterfaceSettingsSection.swift in Sources */, 4C464BF42187AF1500D30602 /* PDFSearchTableViewCell.swift in Sources */, @@ -2471,16 +2542,19 @@ 4C464BF32187AF1500D30602 /* PDFOutlineViewController.swift in Sources */, DC85572C20513B8C00189B9A /* ServerListTableViewController.swift in Sources */, 233BDEA0204FEFE500C06732 /* AppDelegate.swift in Sources */, + 4C51727E22DE04BD001BC97F /* BackgroundFetchUpdateTaskAction.swift in Sources */, 236735A621217C3500E5834A /* MoreViewController.swift in Sources */, 39E2FE0021FF814A00F0117F /* ThemeRoundedButton.swift in Sources */, 23957A6D209AFFE8003C8537 /* MoreSettingsSection.swift in Sources */, 4C464BEF2187AF1500D30602 /* PDFThumbnailCollectionViewCell.swift in Sources */, 232B01F62126B10900366FA0 /* MoreStaticTableViewController.swift in Sources */, 6E91F37E21ECA6FD009436D2 /* CopyAction.swift in Sources */, + 4CC4A21922FB4F4C00AE7E2C /* MediaUploadQueue.swift in Sources */, 593BAB97209F8A0500023634 /* AppLockManager.swift in Sources */, 4CC46D212284C677009E938F /* BookmarkInfoViewController.swift in Sources */, DC3393A022E0A1C000DD3DA4 /* ItemPolicyTableViewController.swift in Sources */, 6E5FC172221590B000F60846 /* DisplayHostViewController.swift in Sources */, + 4C51727F22DE04BD001BC97F /* ScheduledTaskManager.swift in Sources */, DC85493421831B0B00782BA8 /* Tools.swift in Sources */, DCFED972208095E200A2D984 /* ClientItemCell.swift in Sources */, 39E98B452279ACF5009911F1 /* PublicLinkEditTableViewController.swift in Sources */, @@ -2491,6 +2565,7 @@ 4C235CEE21F88C0300A989A8 /* UIViewController+Extension.swift in Sources */, 23F6238120B587EF004FDE8B /* SortMethod.swift in Sources */, DC27A19D20CAB602008ACB6C /* FileProviderInterfaceManager.swift in Sources */, + 4C51727D22DE04BD001BC97F /* ScheduledTaskExtension.swift in Sources */, DCC085512293ED52008CC05C /* DisplaySettingsSection.swift in Sources */, 23EC77582137F3DD0032D4E6 /* PDFViewerViewController.swift in Sources */, 239F1319205A693A0029F186 /* UIColor+Extension.swift in Sources */, @@ -2499,6 +2574,7 @@ DC3BE0E12077CD4B002A0AC0 /* Synchronized.swift in Sources */, DCE5E8B82080D8D9005F60CE /* OCItem+Extension.swift in Sources */, 232F7CAD2097140300EE22E4 /* UploadsSettingsSection.swift in Sources */, + 4CB8ADDE22DF5D3700F1FEBC /* PHPhotoLibrary+Extension.swift in Sources */, DCB44D852186FEF700DAA4CC /* ThemeStyle+DefaultStyles.swift in Sources */, 597A404920AD59EF00B028B2 /* AppLockWindow.swift in Sources */, 6EA78B8F2179B55400A5216A /* ImageScrollView.swift in Sources */, diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/MakeTVG.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/MakeTVG.xcscheme index 2b09b09e9..ff7a95bcf 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/MakeTVG.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/MakeTVG.xcscheme @@ -1,6 +1,6 @@ + RemotePath = "/Users/felix/Library/Developer/CoreSimulator/Devices/348A2D78-5A15-4E50-8EAF-1170115BC798/data/Containers/Bundle/Application/AD282E5C-E658-4707-8C08-A292831061C2/ownCloud.app"> + + diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File ProviderUI.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File ProviderUI.xcscheme index 1820c1c52..2e28d0f3e 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File ProviderUI.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud File ProviderUI.xcscheme @@ -1,6 +1,6 @@ + + . +* +*/ + +import AVFoundation + +extension AVAsset { + func exportVideo(targetURL:URL, type:AVFileType, completion:@escaping (_ success:Bool) -> Void) { + if self.isExportable { + + let preset = AVAssetExportPresetHighestQuality + + AVAssetExportSession.determineCompatibility(ofExportPreset: preset, with: self, outputFileType: type, completionHandler: { (isCompatible) in + if !isCompatible { + completion(false) + }}) + + guard let export = AVAssetExportSession(asset: self, presetName: preset) else { + completion(false) + return + } + + export.outputFileType = type + export.outputURL = targetURL + export.exportAsynchronously { + completion( export.status == .completed ) + } + } else { + completion(false) + } + } +} diff --git a/ownCloud/AppDelegate.swift b/ownCloud/AppDelegate.swift index 5cfeab936..9e1b892ca 100644 --- a/ownCloud/AppDelegate.swift +++ b/ownCloud/AppDelegate.swift @@ -53,8 +53,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { FileProviderInterfaceManager.shared.updateDomainsFromBookmarks() - // Set up background refresh - application.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum + 10) + ScheduledTaskManager.shared.setup() // Display Extensions OCExtensionManager.shared.addExtension(WebViewDisplayViewController.displayExtension) @@ -76,6 +75,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { OCExtensionManager.shared.addExtension(MakeAvailableOfflineAction.actionExtension) OCExtensionManager.shared.addExtension(MakeUnavailableOfflineAction.actionExtension) + OCExtensionManager.shared.addExtension(BackgroundFetchUpdateTaskAction.taskExtension) + OCExtensionManager.shared.addExtension(InstantMediaUploadTaskExtension.taskExtension) + Theme.shared.activeCollection = ThemeCollection(with: ThemeStyle.preferredStyle) // Licenses @@ -86,6 +88,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UIView.setAnimationsEnabled(enableUIAnimations) } + // Set background refresh interval + UIApplication.shared.setMinimumBackgroundFetchInterval( + UIApplication.backgroundFetchIntervalMinimum) + return true } @@ -101,11 +107,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - Log.debug("AppDelegate: performFetchWithCompletionHandler") - - OnMainThread(after: 2.0) { - completionHandler(.noData) - } + ScheduledTaskManager.shared.backgroundFetch(completionHandler: completionHandler) } func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { diff --git a/ownCloud/Client/Actions/Actions+Extensions/UploadBaseAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UploadBaseAction.swift index b1cbf8fb3..09c4f0029 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/UploadBaseAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/UploadBaseAction.swift @@ -22,9 +22,6 @@ import ownCloudApp class UploadBaseAction: Action { - typealias UploadCompletionHandler = (_ success: Bool, _ item:OCItem?) -> Void - typealias UploadPlaceholderCompletionHandler = (_ item:OCItem?, _ error:Error?) -> Void - // MARK: - Action Matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { // Only available for a single item .. @@ -40,36 +37,14 @@ class UploadBaseAction: Action { return .middle } - // MARK: - Upload - func upload(itemURL: URL, to rootItem: OCItem, name: String, completionHandler: UploadCompletionHandler? = nil, placeholderHandler:UploadPlaceholderCompletionHandler? = nil, importByCopy:Bool = false) { - if let progress = core?.importItemNamed(name, - at: rootItem, - from: itemURL, - isSecurityScoped: false, - options: [ - OCCoreOption.importByCopying : importByCopy, - OCCoreOption.automaticConflictResolutionNameStyle : OCCoreDuplicateNameStyle.bracketed.rawValue - ], - placeholderCompletionHandler: { (error, item) in - if error != nil { - Log.debug("Error uploading \(Log.mask(name)) to \(Log.mask(rootItem.path)), error: \(error?.localizedDescription ?? "" )") - } - placeholderHandler?(item, error) - }, - resultHandler: { (error, _ core, _ item, _) in - if error != nil { - Log.debug("Error uploading \(Log.mask(name)) to \(Log.mask(rootItem.path)), error: \(error?.localizedDescription ?? "" )") - completionHandler?(false, item) - } else { - Log.debug("Success uploading \(Log.mask(name)) to \(Log.mask(rootItem.path))") - completionHandler?(true, item) - } - } - ) { + internal func upload(itemURL: URL, to rootItem: OCItem, name: String) -> Bool { + + if core != nil, let progress = itemURL.upload(with: core, at: rootItem) { self.publish(progress: progress) + return true } else { Log.debug("Error setting up upload of \(Log.mask(name)) to \(Log.mask(rootItem.path))") - completionHandler?(false, nil) + return false } } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/UploadFileAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UploadFileAction.swift index a01c5029b..d60b8e8e7 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/UploadFileAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/UploadFileAction.swift @@ -63,7 +63,10 @@ extension UploadFileAction : UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { if let rootItem = context.items.first { for url in urls { - self.upload(itemURL: url, to: rootItem, name: url.lastPathComponent) + if !self.upload(itemURL: url, to: rootItem, name: url.lastPathComponent) { + self.completed(with: NSError(ocError: .internal)) + return + } } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift index 0c45a42c7..eb0d876ed 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift @@ -19,6 +19,7 @@ import UIKit import ownCloudSDK import Photos +import MobileCoreServices class UploadMediaAction: UploadBaseAction { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.uploadphotos") } @@ -26,14 +27,10 @@ class UploadMediaAction: UploadBaseAction { override class var name : String { return "Upload from your photo library".localized } override class var locations : [OCExtensionLocationIdentifier]? { return [.folderAction] } - private let uploadSerialQueue = DispatchQueue(label: "com.owncloud.upload.queue", target: DispatchQueue.global(qos: .background)) - private struct AssociatedKeys { static var actionKey = "action" } - private enum OutputImageFormat { case HEIF, JPEG} - // MARK: - Action implementation override func run() { guard context.items.count == 1, context.items.first?.type == .collection, let viewController = context.viewController else { @@ -41,252 +38,38 @@ class UploadMediaAction: UploadBaseAction { return } - let permisson = PHPhotoLibrary.authorizationStatus() - - switch permisson { - case .authorized: - presentImageGalleryPicker() - - case .notDetermined: - PHPhotoLibrary.requestAuthorization({ newStatus in - if newStatus == .authorized { - self.presentImageGalleryPicker() - } else { - self.completed() - } - }) - - default: - PHPhotoLibrary.requestAuthorization({ newStatus in - if newStatus == .denied { - let alert = UIAlertController(title: "Missing permissions".localized, message: "This permission is needed to upload photos and videos from your photo library.".localized, preferredStyle: .alert) - - let settingAction = UIAlertAction(title: "Settings".localized, style: .default, handler: { _ in - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) - }) - let notNowAction = UIAlertAction(title: "Not now".localized, style: .cancel) - - alert.addAction(settingAction) - alert.addAction(notNowAction) - - OnMainThread { - viewController.present(alert, animated: true) - - self.completed() - } - } - }) - } - } - - private func presentImageGalleryPicker() { - OnMainThread { - if let viewController = self.context.viewController { - let photoAlbumViewController = PhotoAlbumTableViewController() - photoAlbumViewController.selectionCallback = { (assets) in - - self.completed() - - let queue = DispatchQueue.global(qos: .userInitiated) - - queue.async { - - self.core?.perform(inRunningCore: { (runningCoreCompletion) in - - let uploadGroup = DispatchGroup() - var uploadFailed = false - - for asset in assets { - if uploadFailed == false { - // Upload image on a background queue - uploadGroup.enter() - - self.upload(asset: asset, completion: { (success) in - if !success { - uploadFailed = true - } - uploadGroup.leave() - }) - - // Avoid submitting to many jobs simultaneously to reduce memory pressure - _ = uploadGroup.wait(timeout: .now() + 0.5) - - } else { - // Escape on first failed download - break - } - } - - uploadGroup.notify(queue: queue, execute: { - runningCoreCompletion() - }) - - }, withDescription: "Uploading \(assets.count) photo assets") - } - } - let navigationController = ThemeNavigationController(rootViewController: photoAlbumViewController) - - viewController.present(navigationController, animated: true) + PHPhotoLibrary.requestAccess { (granted) in + if granted { + self.presentImageGalleryPicker() } else { - self.completed(with: NSError(ocError: .internal)) - } - } - } - - private func convertImage(_ sourceURL:URL, targetURL:URL, outputFormat:OutputImageFormat) -> Bool { - // Conversion to JPEG required - let colorSpace = CGColorSpaceCreateDeviceRGB() - var ciContext = CIContext() - var imageData : Data? - - var image = CIImage(contentsOf: sourceURL) - - func cleanUpCoreImageRessources() { - // Release memory consuming resources - imageData = nil - image = nil - ciContext.clearCaches() - } - - if image != nil { - switch outputFormat { - case .JPEG: - imageData = ciContext.jpegRepresentation(of: image!, colorSpace: colorSpace) - case .HEIF: - imageData = ciContext.heifRepresentation(of: image!, format: CIFormat.RGBA8, colorSpace: colorSpace) - } - - if imageData != nil { - do { - // First write an image to a file stored in temporary directory - try imageData!.write(to: targetURL) - cleanUpCoreImageRessources() - return true - } catch { - cleanUpCoreImageRessources() - } + let alert = UIAlertController.alertControllerForPhotoLibraryAuthorizationInSettings() + viewController.present(alert, animated: true) + self.completed() } } - - return false } - private func exportVideoAsset(_ asset:AVAsset, targetURL:URL, type:AVFileType, completion:@escaping (_ success:Bool) -> Void) { - if asset.isExportable { - - let preset = AVAssetExportPresetHighestQuality + private func presentImageGalleryPicker() { + if let viewController = self.context.viewController { + let photoAlbumViewController = PhotoAlbumTableViewController() + photoAlbumViewController.selectionCallback = {(assets) in + self.completed() - AVAssetExportSession.determineCompatibility(ofExportPreset: preset, with: asset, outputFileType: type, completionHandler: { (isCompatible) in - if !isCompatible { - completion(false) - }}) + guard let rootItem = self.context.items.first else { return } - guard let export = AVAssetExportSession(asset: asset, presetName: preset) else { - completion(false) - return + MediaUploadQueue.shared.uploadAssets(assets, with: self.core, at: rootItem, progressHandler: { (progress) in + if progress.isFinished || progress.isCancelled { + self.unpublish(progress: progress) + } else { + self.publish(progress: progress) + } + }) } + let navigationController = ThemeNavigationController(rootViewController: photoAlbumViewController) - export.outputFileType = type - export.outputURL = targetURL - export.exportAsynchronously { - completion( export.status == .completed ) - } + viewController.present(navigationController, animated: true) } else { - completion(false) - } - } - - private func upload(asset:PHAsset, completion:@escaping (_ success:Bool) -> Void ) { - guard let userDefaults = OCAppIdentity.shared.userDefaults else { return } - - guard let rootItem = context.items.first else { return } - - // Prepare progress object for importing full size asset from photo library - let progress = Progress(totalUnitCount: 100) - progress.localizedDescription = "Importing from photo library".localized - self.publish(progress: progress) - - // Setup import options, allow download asset from network if necessary - let contentInputOptions = PHContentEditingInputRequestOptions() - contentInputOptions.isNetworkAccessAllowed = true - contentInputOptions.progressHandler = { (percentage:Double, _) in - progress.completedUnitCount = Int64(percentage * 100) - } - - // Import full size asset - asset.requestContentEditingInput(with: contentInputOptions) { (input, _) in - self.unpublish(progress: progress) - - var assetURL: URL? - switch asset.mediaType { - case .image: - assetURL = input?.fullSizeImageURL - case .video: - assetURL = (input?.audiovisualAsset as? AVURLAsset)?.url - default: - break - } - - self.uploadSerialQueue.async { - if let input = input, let url = assetURL { - - func performUpload(sourceURL:URL, copySource:Bool) { - - @discardableResult func removeSourceFile() -> Bool { - do { - try FileManager.default.removeItem(at: sourceURL) - return true - } catch { - return false - } - } - - let fileName = sourceURL.lastPathComponent - self.upload(itemURL: sourceURL, to: rootItem, name:fileName, placeholderHandler: { (_, error) in - if !copySource && error != nil { - // Delete the temporary asset file in case of critical error - removeSourceFile() - } - completion(error == nil) - }, importByCopy: copySource) - } - - if asset.mediaType == .image { - if !userDefaults.convertHeic || input.uniformTypeIdentifier == "public.jpeg" { - // No conversion of the image data, upload as is - performUpload(sourceURL: url, copySource: true) - } else { - let fileName = url.lastPathComponent - let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension().appendingPathExtension("jpg") - // Convert to JPEG - if self.convertImage(url, targetURL: localURL, outputFormat: .JPEG) { - // Upload to the cloud - performUpload(sourceURL: localURL, copySource: false) - } else { - completion(false) - } - } - } else if asset.mediaType == .video { - if userDefaults.convertVideosToMP4 { - let fileName = url.lastPathComponent - let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension().appendingPathExtension("mp4") - self.exportVideoAsset(input.audiovisualAsset!, targetURL: localURL, type: .mp4, completion: { (exportSuccess) in - if exportSuccess { - performUpload(sourceURL: localURL, copySource: false) - } else { - completion(false) - } - }) - - } else { - performUpload(sourceURL: url, copySource: true) - } - } - - } else { - completion(false) - } - } + self.completed(with: NSError(ocError: .internal)) } } diff --git a/ownCloud/CoreImage Extensions/CIImage+Extensions.swift b/ownCloud/CoreImage Extensions/CIImage+Extensions.swift new file mode 100644 index 000000000..a5ad44d63 --- /dev/null +++ b/ownCloud/CoreImage Extensions/CIImage+Extensions.swift @@ -0,0 +1,57 @@ +// +// CIImage+Extensions.swift +// ownCloud +// +// Created by Michael Neuwert on 17.07.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, 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 CoreImage + +extension CIImage { + + enum OutputImageFormat { case HEIF, JPEG} + + func convert(targetURL:URL, outputFormat:OutputImageFormat) -> Bool { + // Conversion to JPEG required + let colorSpace = CGColorSpaceCreateDeviceRGB() + var ciContext = CIContext() + var imageData : Data? + + func cleanUpCoreImageRessources() { + // Release memory consuming resources + imageData = nil + ciContext.clearCaches() + } + + switch outputFormat { + case .JPEG: + imageData = ciContext.jpegRepresentation(of: self, colorSpace: colorSpace) + case .HEIF: + imageData = ciContext.heifRepresentation(of: self, format: CIFormat.RGBA8, colorSpace: colorSpace) + } + + if imageData != nil { + do { + // First write an image to a file stored in temporary directory + try imageData!.write(to: targetURL) + cleanUpCoreImageRessources() + return true + } catch { + cleanUpCoreImageRessources() + } + } + + return false + } +} diff --git a/ownCloud/FileProvider Integration/FileProviderInterfaceManager.swift b/ownCloud/FileProvider Integration/FileProviderInterfaceManager.swift index 318fbb319..dc4a36658 100644 --- a/ownCloud/FileProvider Integration/FileProviderInterfaceManager.swift +++ b/ownCloud/FileProvider Integration/FileProviderInterfaceManager.swift @@ -43,6 +43,8 @@ class FileProviderInterfaceManager: NSObject { } func updateDomainsFromBookmarks() { + if !OCVault.hostHasFileProvider { return } + NSFileProviderManager.getDomainsWithCompletionHandler { (fileProviderDomains, error) in OnMainThread { if error != nil { diff --git a/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift b/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift new file mode 100644 index 000000000..6621b9089 --- /dev/null +++ b/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift @@ -0,0 +1,96 @@ +// +// MediaUploadQueue.swift +// ownCloud +// +// Created by Michael Neuwert on 07.08.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 Foundation +import ownCloudSDK +import Photos +import MobileCoreServices + +class MediaUploadQueue { + private let uploadSerialQueue = DispatchQueue(label: "com.owncloud.upload.queue", target: DispatchQueue.global(qos: .background)) + + static let shared = MediaUploadQueue() + + func uploadAssets(_ assets:[PHAsset], with core:OCCore?, at rootItem:OCItem, progressHandler:((Progress) -> Void)? = nil, assetUploadCompletion:((_ asset:PHAsset?, _ finished:Bool) -> Void)? = nil ) { + + let backgroundTask = OCBackgroundTask(name: "UploadMediaAction", expirationHandler: { (bgTask) in + Log.warning("UploadMediaAction background task expired") + bgTask.end() + }).start() + + let queue = DispatchQueue.global(qos: .userInitiated) + + weak var weakCore = core + + queue.async { + + guard let userDefaults = OCAppIdentity.shared.userDefaults else { return } + + var prefferedMediaOutputFormats = [String]() + if userDefaults.convertHeic { + prefferedMediaOutputFormats.append(String(kUTTypeJPEG)) + } + if userDefaults.convertVideosToMP4 { + prefferedMediaOutputFormats.append(String(kUTTypeMPEG4)) + } + + let uploadGroup = DispatchGroup() + var uploadFailed = false + + for asset in assets { + if uploadFailed == false { + self.uploadSerialQueue.async { + if weakCore != nil { + uploadGroup.enter() + weakCore!.perform(inRunningCore: { (runningCoreCompletion) in + asset.upload(with: weakCore!, at: rootItem, preferredFormats: prefferedMediaOutputFormats, completionHandler: { (item, _) in + if item == nil { + uploadFailed = true + } else { + assetUploadCompletion?(asset, false) + } + runningCoreCompletion() + uploadGroup.leave() + }, progressHandler: { (progress) in + progressHandler?(progress) + }) + }, withDescription: "Uploading \(assets.count) photo assets") + + // Avoid submitting to many jobs simultaneously to reduce memory pressure + _ = uploadGroup.wait() + + } else { + // Core reference became nil + uploadFailed = true + } + } + + } else { + // Escape on first failed download + break + } + } + + uploadGroup.notify(queue: queue, execute: { + backgroundTask?.end() + assetUploadCompletion?(nil, true) + }) + } + } + +} diff --git a/ownCloud/PhotoKit Extensions/PHAsset+Upload.swift b/ownCloud/PhotoKit Extensions/PHAsset+Upload.swift new file mode 100644 index 000000000..66a69f3ed --- /dev/null +++ b/ownCloud/PhotoKit Extensions/PHAsset+Upload.swift @@ -0,0 +1,157 @@ +// +// PHAsset+Upload.swift +// ownCloud +// +// Created by Michael Neuwert on 17.07.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, 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 Photos +import ownCloudSDK +import CoreServices + +extension PHAsset { + /** + Method for uploading assets from photo library to oC instance + - parameter core: Reference to the core to be used for the upload + - parameter rootItem: Directory item where the media file shall be uploaded + - parameter preferredFormats: Array of UTI identifiers describing desired output formats + - parameter completionHandler: Completion handler called after the media file is imported into the core and placeholder item is created. + - parameter progressHandler: Receives progress of the at the moment running activity + */ + func upload(with core:OCCore?, at rootItem:OCItem, preferredFormats:[String]? = nil, completionHandler:@escaping (_ item:OCItem?, _ error:Error?) -> Void, progressHandler:((_ progress:Progress) -> Void)? = nil) { + + func performUpload(sourceURL:URL, copySource:Bool) { + + @discardableResult func removeSourceFile() -> Bool { + do { + try FileManager.default.removeItem(at: sourceURL) + return true + } catch { + return false + } + } + + var uploadProgress: Progress? + + uploadProgress = sourceURL.upload(with: core, + at: rootItem, + importByCopy: copySource, + placeholderHandler: { (item, error) in + if !copySource && error != nil { + // Delete the temporary asset file in case of critical error + removeSourceFile() + } + completionHandler(item, error) + + }, completionHandler: { (_, _) in + if uploadProgress != nil { + progressHandler?(uploadProgress!) + } + }) + + if uploadProgress != nil { + progressHandler?(uploadProgress!) + } + } + + // Prepare progress object for importing full size asset from photo library + let importProgress = Progress(totalUnitCount: 100) + importProgress.localizedDescription = "Importing from photo library".localized + + // Setup import options, allow download asset from network if necessary + let contentInputOptions = PHContentEditingInputRequestOptions() + contentInputOptions.isNetworkAccessAllowed = true + + _ = autoreleasepool { + self.requestContentEditingInput(with: contentInputOptions) { (contentInput, requestInfo) in + + var supportedConversionFormats = Set() + + if let input = contentInput { + + // Determine the correct source URL based on media type + var assetUTI: String? + var assetURL: URL? + switch self.mediaType { + case .image: + assetURL = input.fullSizeImageURL + assetUTI = input.uniformTypeIdentifier + supportedConversionFormats.insert(String(kUTTypeJPEG)) + case .video: + assetURL = (input.audiovisualAsset as? AVURLAsset)?.url + assetUTI = PHAssetResource.assetResources(for: self).first?.uniformTypeIdentifier + supportedConversionFormats.insert(String(kUTTypeMPEG4)) + default: + break + } + + guard let url = assetURL else { return } + + let fileName = url.lastPathComponent + + // Check if the conversion was requested and current media format is not found in the list of requested formats + if let formats = preferredFormats, formats.count > 0 { + if assetUTI != nil, !formats.contains(assetUTI!) { + // Conversion is required + if let outputFormat = formats.first(where: { supportedConversionFormats.contains($0) }) { + + switch (self.mediaType, outputFormat) { + case (.video, String(kUTTypeMPEG4)): + if let avAsset = input.audiovisualAsset { + let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension().appendingPathExtension("mp4") + avAsset.exportVideo(targetURL: localURL, type: .mp4, completion: { (exportSuccess) in + if exportSuccess { + performUpload(sourceURL: localURL, copySource: false) + } else { + completionHandler(nil, NSError(ocError: .internal)) + } + }) + } + case (.image, String(kUTTypeJPEG)): + let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName).deletingPathExtension().appendingPathExtension("jpg") + var imageConverted = false + + if let image = CIImage(contentsOf: assetURL!) { + imageConverted = image.convert(targetURL: localURL, outputFormat: .JPEG) + } + + if imageConverted { + performUpload(sourceURL: localURL, copySource: false) + } else { + completionHandler(nil, NSError(ocError: .internal)) + } + default: + break + } + + } else { + completionHandler(nil, NSError(ocError: .internal)) + } + } else { + performUpload(sourceURL: url, copySource: true) + } + } else { + performUpload(sourceURL: url, copySource: true) + } + + } else { + // If no content was returned check request info dictionary + let error = requestInfo[PHContentEditingInputErrorKey] as? NSError + completionHandler(nil, error) + } + } + } + } + +} diff --git a/ownCloud/PhotoKit Extensions/PHPhotoLibrary+Extension.swift b/ownCloud/PhotoKit Extensions/PHPhotoLibrary+Extension.swift new file mode 100644 index 000000000..e0594ebec --- /dev/null +++ b/ownCloud/PhotoKit Extensions/PHPhotoLibrary+Extension.swift @@ -0,0 +1,49 @@ +// +// PHPhotoLibrary+Extension.swift +// ownCloud +// +// Created by Michael Neuwert on 17.07.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, 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 UIKit +import Photos + +extension PHPhotoLibrary { + + class func requestAccess(completion:@escaping (_ accessGranted:Bool) -> Void) { + let permisson = PHPhotoLibrary.authorizationStatus() + + func requestAuthorization() { + PHPhotoLibrary.requestAuthorization({ newStatus in + let authorized = newStatus == .authorized ? true : false + OnMainThread { + completion(authorized) + } + }) + } + + switch permisson { + case .authorized: + OnMainThread { + completion(true) + } + + case .notDetermined: + requestAuthorization() + + default: + requestAuthorization() + } + } +} diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index 2917080f4..e5c3ff4c1 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -405,6 +405,14 @@ "Media Upload" = "Media Upload"; "Convert HEIC to JPEG" = "Convert HEIC to JPEG"; "Convert videos to MP4" = "Convert videos to MP4"; +"Instant Upload Photos" = "Instant Upload Photos"; +"Instant Upload Videos" = "Instant Upload Videos"; +"Account" = "Account"; +"Accounts" = "Accounts"; +"Select account" = "Select account"; +"Upload Path" = "Upload Path"; +"Select Upload Path" = "Select Upload Path"; +"Instant upload of media was disabled since configured account / folder was not found" = "Instant upload of media was disabled since configured account / folder was not found"; /* Progress summarizer */ "Creating %ld folders…" = "Creating %ld folders…"; diff --git a/ownCloud/Server List/ServerListTableViewController.swift b/ownCloud/Server List/ServerListTableViewController.swift index f1395a5e9..9f03fe203 100644 --- a/ownCloud/Server List/ServerListTableViewController.swift +++ b/ownCloud/Server List/ServerListTableViewController.swift @@ -46,6 +46,7 @@ class ServerListTableViewController: UITableViewController, Themeable { deinit { NotificationCenter.default.removeObserver(self, name: .OCBookmarkManagerListChanged, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) } // TODO: Rebuild welcomeOverlayView in code @@ -89,6 +90,8 @@ class ServerListTableViewController: UITableViewController, Themeable { welcomeLogoTVGView.vectorImage = Theme.shared.tvgImage(for: "owncloud-logo") self.navigationItem.title = OCAppIdentity.shared.appName + + NotificationCenter.default.addObserver(self, selector: #selector(considerAutoLogin), name: UIApplication.didBecomeActiveNotification, object: nil) } override func viewWillAppear(_ animated: Bool) { @@ -123,18 +126,26 @@ class ServerListTableViewController: UITableViewController, Themeable { settingsBarButtonItem ] - if shownFirstTime { + if showBetaWarning, shownFirstTime { + showBetaWarning = !considerAutoLogin() + } + + if showBetaWarning { + considerBetaWarning() + } + } + + @objc func considerAutoLogin() -> Bool { + if shownFirstTime, UIApplication.shared.applicationState != .background { shownFirstTime = false if let bookmark = OCBookmarkManager.lastBookmarkSelectedForConnection { connect(to: bookmark) - showBetaWarning = false + return true } } - if showBetaWarning { - considerBetaWarning() - } + return false } func considerBetaWarning() { diff --git a/ownCloud/Settings/MediaUploadSettingsSection.swift b/ownCloud/Settings/MediaUploadSettingsSection.swift index f3423ffc3..f68696eb8 100644 --- a/ownCloud/Settings/MediaUploadSettingsSection.swift +++ b/ownCloud/Settings/MediaUploadSettingsSection.swift @@ -17,14 +17,24 @@ */ import UIKit +import Photos +import ownCloudSDK extension UserDefaults { enum MediaUploadKeys : String { case ConvertHEICtoJPEGKey = "convert-heic-to-jpeg" case ConvertVideosToMP4Key = "convert-videos-to-mp4" + case InstantUploadPhotosKey = "instant-upload-photos" + case InstantUploadVideosKey = "instant-upload-videos" + case InstantUploadBookmarkUUIDKey = "instant-upload-bookmark-uuid" + case InstantUploadPathKey = "instant-upload-path" + case InstantUploadPhotosAfterDateKey = "instant-upload-photos-after-date" + case InstantUploadVideosAfterDateKey = "instant-upload-videos-after-date" } + static let MediaUploadSettingsChangedNotification = NSNotification.Name("settings.media-upload-settings-changed") + public var convertHeic: Bool { set { self.set(newValue, forKey: MediaUploadKeys.ConvertHEICtoJPEGKey.rawValue) @@ -44,12 +54,100 @@ extension UserDefaults { return self.bool(forKey: MediaUploadKeys.ConvertVideosToMP4Key.rawValue) } } + + public var instantUploadPhotos: Bool { + set { + self.set(newValue, forKey: MediaUploadKeys.InstantUploadPhotosKey.rawValue) + } + + get { + return self.bool(forKey: MediaUploadKeys.InstantUploadPhotosKey.rawValue) + } + } + + public var instantUploadVideos: Bool { + set { + self.set(newValue, forKey: MediaUploadKeys.InstantUploadVideosKey.rawValue) + } + + get { + return self.bool(forKey: MediaUploadKeys.InstantUploadVideosKey.rawValue) + } + } + + public var instantUploadBookmarkUUID: UUID? { + set { + self.set(newValue?.uuidString, forKey: MediaUploadKeys.InstantUploadBookmarkUUIDKey.rawValue) + } + + get { + if let uuidString = self.string(forKey: MediaUploadKeys.InstantUploadBookmarkUUIDKey.rawValue) { + return UUID(uuidString: uuidString) + } else { + return nil + } + } + } + + public var instantUploadPath: String? { + + set { + self.set(newValue, forKey: MediaUploadKeys.InstantUploadPathKey.rawValue) + } + + get { + return self.string(forKey: MediaUploadKeys.InstantUploadPathKey.rawValue) + } + } + + public var instantUploadPhotosAfter: Date? { + set { + self.set(newValue, forKey: MediaUploadKeys.InstantUploadPhotosAfterDateKey.rawValue) + } + + get { + return self.value(forKey: MediaUploadKeys.InstantUploadPhotosAfterDateKey.rawValue) as? Date + } + } + + public var instantUploadVideosAfter: Date? { + set { + self.set(newValue, forKey: MediaUploadKeys.InstantUploadVideosAfterDateKey.rawValue) + } + + get { + return self.value(forKey: MediaUploadKeys.InstantUploadVideosAfterDateKey.rawValue) as? Date + } + } + + public func resetInstantUploadConfiguration() { + self.instantUploadBookmarkUUID = nil + self.instantUploadPath = nil + self.instantUploadPhotos = false + self.instantUploadVideos = false + } } class MediaUploadSettingsSection: SettingsSection { + private static let bookmarkAndPathSelectionRowIdentifier = "bookmarkAndPathSelectionRowIdentifier" + private var convertPhotosSwitchRow: StaticTableViewRow? private var convertVideosSwitchRow: StaticTableViewRow? + private var instantUploadPhotosRow: StaticTableViewRow? + private var instantUploadVideosRow: StaticTableViewRow? + + private var bookmarkAndPathSelectionRow: StaticTableViewRow? + + private var uploadLocationSelected : Bool { + if self.userDefaults.instantUploadBookmarkUUID != nil && self.userDefaults.instantUploadPath != nil { + return true + } else { + return false + } + } + + private var uploadPathTracking: OCCoreItemTracking? override init(userDefaults: UserDefaults) { @@ -72,6 +170,201 @@ class MediaUploadSettingsSection: SettingsSection { self.add(row: convertPhotosSwitchRow!) self.add(row: convertVideosSwitchRow!) + + // Instant upload requires at least one configured account + if OCBookmarkManager.shared.bookmarks.count > 0 { + instantUploadPhotosRow = StaticTableViewRow(switchWithAction: { [weak self] (_, sender) in + if let convertSwitch = sender as? UISwitch { + self?.changeAndRequestPhotoLibraryAccessForOption(optionSwitch: convertSwitch, completion: { (switchState) in + self?.userDefaults.instantUploadPhotos = switchState + self?.userDefaults.instantUploadPhotosAfter = switchState ? Date() : nil + + if switchState, let locationSelected = self?.uploadLocationSelected, locationSelected == false { + self?.showAccountSelectionViewController() + } else { + self?.postSettingsChangedNotification() + } + + }) + } + }, title: "Instant Upload Photos".localized, value: self.userDefaults.instantUploadPhotos) + + instantUploadVideosRow = StaticTableViewRow(switchWithAction: { [weak self] (_, sender) in + if let convertSwitch = sender as? UISwitch { + self?.changeAndRequestPhotoLibraryAccessForOption(optionSwitch: convertSwitch, completion: { (switchState) in + self?.userDefaults.instantUploadVideos = switchState + self?.userDefaults.instantUploadVideosAfter = switchState ? Date() : nil + + if switchState, let locationSelected = self?.uploadLocationSelected, locationSelected == false { + self?.showAccountSelectionViewController() + } else { + self?.postSettingsChangedNotification() + } + }) + } + }, title: "Instant Upload Videos".localized, value: self.userDefaults.instantUploadVideos) + + bookmarkAndPathSelectionRow = StaticTableViewRow(valueRowWithAction: { [weak self] (_, _) in + self?.showAccountSelectionViewController() + }, title: "Upload Path".localized, value: "", accessoryType: .disclosureIndicator, identifier: MediaUploadSettingsSection.bookmarkAndPathSelectionRowIdentifier) + + self.add(row: instantUploadPhotosRow!) + self.add(row: instantUploadVideosRow!) + + updateDynamicUI() + } + } + + private func getSelectedBookmark() -> OCBookmark? { + if let selectedBookmarkUUID = self.userDefaults.instantUploadBookmarkUUID { + let bookmarks : [OCBookmark] = OCBookmarkManager.shared.bookmarks as [OCBookmark] + return bookmarks.filter({ $0.uuid == selectedBookmarkUUID}).first + } + return nil + } + + private func updateDynamicUI() { + + func updateInstantUploadSwtches() { + OnMainThread { + self.instantUploadPhotosRow?.value = self.userDefaults.instantUploadPhotos + self.instantUploadVideosRow?.value = self.userDefaults.instantUploadVideos + } + } + + self.remove(rowWithIdentifier: MediaUploadSettingsSection.bookmarkAndPathSelectionRowIdentifier) + + if let bookmark = getSelectedBookmark(), let path = self.userDefaults.instantUploadPath { + + OCCoreManager.shared.requestCore(for: bookmark, setup: { (_, _) in }, + completionHandler: { (core, error) in + if core != nil, error == nil { + core?.fetchUpdates(completionHandler: { (fetchError, _) in + if fetchError == nil { + self.uploadPathTracking = core?.trackItem(atPath: path, trackingHandler: { (_, pathItem, isInitial) in + if isInitial { + if pathItem != nil { + OnMainThread { + self.add(row: self.bookmarkAndPathSelectionRow!) + let directory = URL(fileURLWithPath: path).lastPathComponent + self.bookmarkAndPathSelectionRow?.value = "\(bookmark.shortName)/\(directory)" + } + } else { + self.userDefaults.resetInstantUploadConfiguration() + OnMainThread { + let alertController = UIAlertController(with: "Instant upload disabled".localized, + message: "Instant upload of media was disabled since configured account / folder was not found".localized) + self.viewController?.present(alertController, animated: true, completion: nil) + } + } + updateInstantUploadSwtches() + OCCoreManager.shared.returnCore(for: bookmark, completionHandler: nil) + } else { + self.uploadPathTracking = nil + } + }) + } else { + OCCoreManager.shared.returnCore(for: bookmark, completionHandler: nil) + + } + }) + } + }) + + } else { + self.userDefaults.resetInstantUploadConfiguration() + updateInstantUploadSwtches() + } + } + + private func changeAndRequestPhotoLibraryAccessForOption(optionSwitch:UISwitch, completion:@escaping (_ value:Bool) -> Void) { + if optionSwitch.isOn { + PHPhotoLibrary.requestAccess(completion: { (granted) in + optionSwitch.isOn = granted + + if !granted { + let alert = UIAlertController.alertControllerForPhotoLibraryAuthorizationInSettings() + self.viewController?.present(alert, animated: true) + } + + completion(granted) + }) + } else { + completion(false) + } + } + + private func showAccountSelectionViewController() { + + let accountSelectionViewController = StaticTableViewController(style: .grouped) + let navigationController = ThemeNavigationController(rootViewController: accountSelectionViewController) + + accountSelectionViewController.navigationItem.title = "Select account".localized + accountSelectionViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, + target: accountSelectionViewController, + action: #selector(accountSelectionViewController.dismissAnimated)) + + let accountsSection = StaticTableViewSection(headerTitle: "Accounts".localized) + + var bookmarkRows: [StaticTableViewRow] = [] + let bookmarks = OCBookmarkManager.shared.bookmarks + + guard bookmarks.count > 0 else { return } + + var bookmarkDictionary = [StaticTableViewRow : OCBookmark]() + + for bookmark in bookmarks { + let row = StaticTableViewRow(buttonWithAction: { [weak self] (_ row, _ sender) in + + let selectedBookmark = bookmarkDictionary[row]! + self?.userDefaults.instantUploadBookmarkUUID = selectedBookmark.uuid + self?.userDefaults.instantUploadPath = nil + + // Proceed with upload path selection + self?.selectUploadPath(for: selectedBookmark, pushIn: navigationController, completion: { (success) in + if !success && self?.userDefaults.instantUploadPath == nil { + self?.userDefaults.resetInstantUploadConfiguration() + } + navigationController.dismiss(animated: true, completion: nil) + self?.postSettingsChangedNotification() + self?.updateDynamicUI() + }) + + }, title: bookmark.shortName, style: .plain, image: Theme.shared.image(for: "owncloud-logo", size: CGSize(width: 25, height: 25)), imageWidth: 25, alignment: .left) + + bookmarkRows.append(row) + bookmarkDictionary[row] = bookmark + } + + accountsSection.add(rows: bookmarkRows) + accountSelectionViewController.addSection(accountsSection) + + self.viewController?.present(navigationController, animated: true) } + private func selectUploadPath(for bookmark:OCBookmark, pushIn navigationController:UINavigationController, completion:@escaping (_ success:Bool) -> Void) { + + OCCoreManager.shared.requestCore(for: bookmark, setup: { (_, _) in }, + completionHandler: { [weak self] (core, error) in + + if let core = core, error == nil { + + OnMainThread { + let directoryPickerViewController = ClientDirectoryPickerViewController(core: core, path: "/", selectButtonTitle: "Select Upload Path".localized, avoidConflictsWith: [], choiceHandler: { (selectedDirectory) in + if selectedDirectory != nil { + self?.userDefaults.instantUploadPath = selectedDirectory?.path + } + OCCoreManager.shared.returnCore(for: bookmark, completionHandler: nil) + + completion(selectedDirectory != nil) + }) + navigationController.pushViewController(directoryPickerViewController, animated: true) + } + } + }) + } + + private func postSettingsChangedNotification() { + NotificationCenter.default.post(name: UserDefaults.MediaUploadSettingsChangedNotification, object: nil) + } } diff --git a/ownCloud/Tasks/BackgroundFetchUpdateTaskAction.swift b/ownCloud/Tasks/BackgroundFetchUpdateTaskAction.swift new file mode 100644 index 000000000..370f361cd --- /dev/null +++ b/ownCloud/Tasks/BackgroundFetchUpdateTaskAction.swift @@ -0,0 +1,86 @@ +// +// BackgroundFetchUpdateTaskAction.swift +// ownCloud +// +// Created by Michael Neuwert on 13.06.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, 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 Foundation +import ownCloudSDK + +class BackgroundFetchUpdateTaskAction : ScheduledTaskAction { + + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.background_update") } + override class var locations : [OCExtensionLocationIdentifier]? { return [.appBackgroundFetch] } + override class var features : [String : Any]? { return [ FeatureKeys.runOnWifi : true] } + + override func run(background:Bool) { + + self.completion = { (task) in + Log.log("Background fetch of updates finished with result \(String(describing: task.result))") + } + + super.run(background: background) + + var errorCount = 0 + var lastError: Error = NSError(ocError: .internal) + let coreUpdateGroup = DispatchGroup() + + // Iterate through bookmarks + for bookmark in OCBookmarkManager.shared.bookmarks { + + // Request cores for the bookmarks and add them to the list + coreUpdateGroup.enter() + OCCoreManager.shared.requestCore(for: bookmark, setup:nil, completionHandler: { (core, error) in + if core != nil { + // Fetch updates from the backend + core?.fetchUpdates(completionHandler: { (error, foundChanges) in + + if foundChanges { + Log.log("Found changes in core \(String(describing: core))") + } + + if error != nil { + lastError = error! + errorCount += 1 + Log.error("fetchUpdates() for \(String(describing: core)) returned with error \(error!)") + } else { + Log.log("Fetched updates for core \(String(describing: core))") + } + + // Give up the core ASAP to minimize traffic + OCCoreManager.shared.returnCore(for: bookmark, completionHandler: { + coreUpdateGroup.leave() + }) + }) + } else { + Log.error("No core returned for bookmark \(bookmark), error: \(String(describing: error))") + errorCount += 1 + // No core returned + coreUpdateGroup.leave() + } + }) + } + + // Handle update completion + coreUpdateGroup.notify(queue: DispatchQueue.main) { + if errorCount == 0 { + self.result = .success(nil) + } else { + self.result = .failure(lastError) + } + self.completed() + } + } +} diff --git a/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift b/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift new file mode 100644 index 000000000..a403e603f --- /dev/null +++ b/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift @@ -0,0 +1,208 @@ +// +// InstantMediaUploadTaskExtension.swift +// ownCloud +// +// Created by Michael Neuwert on 24.07.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, 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 Foundation +import ownCloudSDK +import Photos + +class InstantMediaUploadTaskExtension : ScheduledTaskAction { + + enum MediaType { + case images, videos, imagesAndVideos + } + + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.instant_media_upload") } + override class var locations : [OCExtensionLocationIdentifier]? { return [.appDidComeToForeground] } + override class var features : [String : Any]? { return [ FeatureKeys.photoLibraryChanged : true, FeatureKeys.runOnWifi : true] } + + private var uploadDirectoryTracking: OCCoreItemTracking? + + override func run(background:Bool) { + guard let userDefaults = OCAppIdentity.shared.userDefaults else { return } + + guard userDefaults.instantUploadPhotos == true || userDefaults.instantUploadVideos == true else { return } + + guard let bookmarkUUID = userDefaults.instantUploadBookmarkUUID else { return } + + guard let path = userDefaults.instantUploadPath else { return } + + if let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID) { + + OCCoreManager.shared.requestCore(for: bookmark, setup:nil, completionHandler: {(core, coreError) in + if core != nil { + + func finalize() { + OCCoreManager.shared.returnCore(for: bookmark, completionHandler: { + self.completed() + }) + } + + core?.fetchUpdates(completionHandler: { (fetchError, _) in + if fetchError == nil { + self.uploadDirectoryTracking = core?.trackItem(atPath: path, trackingHandler: { (error, item, isInitial) in + + if isInitial { + if error != nil { + Log.error("Error \(String(describing: error))") + } + + if item != nil { + self.uploadMediaAssets(with: core, at: item!, completion: { + finalize() + }) + } else { + Log.warning("Instant upload directory not found") + userDefaults.resetInstantUploadConfiguration() + finalize() + self.showFeatureDisabledAlert() + } + } else { + self.uploadDirectoryTracking = nil + } + }) + } else { + Log.error("Fetching bookmark update failed with \(String(describing: fetchError))") + finalize() + } + }) + } else { + if coreError != nil { + Log.error("No core returned with error \(String(describing: coreError))") + self.result = .failure(coreError!) + } + self.completed() + } + }) + } + } + + private func uploadMediaAssets(with core:OCCore?, at item:OCItem, completion:@escaping () -> Void) { + guard let userDefaults = OCAppIdentity.shared.userDefaults else { return } + + var assets = [PHAsset]() + + // Add photo assets + if let uploadPhotosAfter = userDefaults.instantUploadPhotosAfter { + let fetchResult = self.fetchAssetsFromCameraRoll(.images, createdAfter: uploadPhotosAfter) + if fetchResult != nil { + fetchResult!.enumerateObjects({ (asset, _, _) in + assets.append(asset) + }) + } + } + + // Add video assets + if let uploadVideosAfter = userDefaults.instantUploadVideosAfter { + let fetchResult = self.fetchAssetsFromCameraRoll(.videos, createdAfter: uploadVideosAfter) + if fetchResult != nil { + fetchResult!.enumerateObjects({ (asset, _, _) in + assets.append(asset) + }) + } + } + + // Perform actual upload operation + if assets.count > 0 { + self.upload(assets: assets, with: core, at: item, completion: { () in + OnMainThread { + completion() + } + }) + } else { + OnMainThread { + completion() + } + } + } + + private func upload(assets:[PHAsset], with core:OCCore?, at rootItem:OCItem, completion:@escaping () -> Void) { + + guard let userDefaults = OCAppIdentity.shared.userDefaults else { return } + + if assets.count > 0 { + Log.debug("Uploading \(assets.count) assets") + MediaUploadQueue.shared.uploadAssets(assets, with: core, at: rootItem, assetUploadCompletion: { (asset, finished) in + if let asset = asset { + switch asset.mediaType { + case .image: + userDefaults.instantUploadPhotosAfter = asset.modificationDate + case .video: + userDefaults.instantUploadVideosAfter = asset.modificationDate + default: + break + } + } + if finished { + completion() + } + }) + } + } + + private func fetchAssetsFromCameraRoll(_ mediaType:MediaType, createdAfter:Date? = nil) -> PHFetchResult? { + + guard PHPhotoLibrary.authorizationStatus() == .authorized else { return nil } + + let collectionResult = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, + subtype: .smartAlbumUserLibrary, + options: nil) + + if let cameraRoll = collectionResult.firstObject { + let imageTypePredicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue) + let videoTypePredicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.video.rawValue) + + var typePredicatesArray = [NSPredicate]() + + switch mediaType { + case .images: + typePredicatesArray.append(imageTypePredicate) + case .videos: + typePredicatesArray.append(videoTypePredicate) + case .imagesAndVideos: + typePredicatesArray.append(imageTypePredicate) + typePredicatesArray.append(videoTypePredicate) + } + + let mediaTypesPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: typePredicatesArray) + + let fetchOptions = PHFetchOptions() + + if let date = createdAfter { + let creationDatePredicate = NSPredicate(format: "modificationDate > %@", date as NSDate) + fetchOptions.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [mediaTypesPredicate, creationDatePredicate]) + } else { + fetchOptions.predicate = mediaTypesPredicate + } + + let sort = NSSortDescriptor(key: "modificationDate", ascending: true) + fetchOptions.sortDescriptors = [sort] + + return PHAsset.fetchAssets(in: cameraRoll, options: fetchOptions) + } + + return nil + } + + private func showFeatureDisabledAlert() { + OnMainThread { + let alertController = UIAlertController(with: "Instant upload disabled".localized, + message: "Instant upload of media was disabled since configured account / folder was not found".localized) + UIApplication.shared.delegate?.window??.rootViewController?.present(alertController, animated: true, completion: nil) + } + } +} diff --git a/ownCloud/Tasks/ScheduledTaskExtension.swift b/ownCloud/Tasks/ScheduledTaskExtension.swift new file mode 100644 index 000000000..63ec0810b --- /dev/null +++ b/ownCloud/Tasks/ScheduledTaskExtension.swift @@ -0,0 +1,119 @@ +// +// ScheduledTaskExtension.swift +// ownCloud +// +// Created by Michael Neuwert on 28.05.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, 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 Foundation +import ownCloudSDK + +extension OCExtensionType { + static let scheduledTask: OCExtensionType = OCExtensionType("app.scheduled_task") //!< Specific identifier for scheduled task extensions +} + +extension OCExtensionLocationIdentifier { + static let appLaunch: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("appLaunch") //!< Application launch + static let appDidBecomeBackgrounded: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("appDidBecomeBackgrounded") //!< Application did come into background + static let appDidComeToForeground: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("appDidComeToForeground") //!< Application did come into foreground + static let appBackgroundFetch: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("appBackgroundFetch") //!< Application woke up to peform background fetch +} + +class ScheduledTaskAction : NSObject { + + struct FeatureKeys { + static let runOnLowBattery: String = "runOnLowBattery" + static let runOnExternalPower: String = "runOnExternalPower" + static let runOnWifi: String = "runOnWifi" + static let photoLibraryChanged: String = "photoLibraryChanged" + } + + typealias ActionResult = Result + typealias ActionHandler = (ScheduledTaskAction) -> Void + + let gracePeriod: TimeInterval = 0.1 + + class var identifier : OCExtensionIdentifier? { return nil } + class var locations : [OCExtensionLocationIdentifier]? { return nil } + class var features : [String : Any]? { return nil } + + var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid + var backgroundFetchCompletion: ((UIBackgroundFetchResult) -> Void)? + + var result : ActionResult? + var completion : ActionHandler? + var runUntil: Date? + + class var taskExtension : ScheduledTaskExtension { + let objectProvider : OCExtensionObjectProvider = { (_ rawExtension, _ context, _ error) -> Any? in + if (rawExtension as? ScheduledTaskExtension) != nil { + return self.init() + } + + return nil + } + + return ScheduledTaskExtension(identifier: identifier!, locations: locations, features: features, objectProvider: objectProvider) + } + + required override init() { + super.init() + } + + func run(background:Bool) { + if background { + self.backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {}) + } + } + + func completed() { + OnMainThread { + self.completion?(self) + + if self.backgroundFetchCompletion != nil { + if let result = self.result { + switch result { + case .success(_): + self.backgroundFetchCompletion!(.newData) + case .failure(_): + self.backgroundFetchCompletion!(.failed) + } + } else { + self.backgroundFetchCompletion!(.noData) + } + self.backgroundFetchCompletion = nil + } + + if self.backgroundTaskIdentifier != .invalid { + UIApplication.shared.endBackgroundTask(self.backgroundTaskIdentifier) + } + } + } + + var allowedToRun : Bool { + if let deadline = runUntil { + if (deadline.timeIntervalSince1970 + gracePeriod) >= Date().timeIntervalSince1970 { + return false + } + } + return true + } +} + +class ScheduledTaskExtension : OCExtension { + init(identifier: OCExtensionIdentifier, locations: [OCExtensionLocationIdentifier]?, features: [String : Any]?, objectProvider: OCExtensionObjectProvider? = nil, customMatcher: OCExtensionCustomContextMatcher? = nil) { + + super.init(identifier: identifier, type: .scheduledTask, locations: locations, features: features, objectProvider: objectProvider, customMatcher: customMatcher) + } +} diff --git a/ownCloud/Tasks/ScheduledTaskManager.swift b/ownCloud/Tasks/ScheduledTaskManager.swift new file mode 100644 index 000000000..2d69f29c6 --- /dev/null +++ b/ownCloud/Tasks/ScheduledTaskManager.swift @@ -0,0 +1,306 @@ +// +// ScheduledTaskManager.swift +// ownCloud +// +// Created by Michael Neuwert on 28.05.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, 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 Foundation +import UIKit +import Network +import Photos +import ownCloudSDK + +class ScheduledTaskManager : NSObject { + enum State { + case launched, foreground, background, backgroundFetch + + func locationIdentifier() -> OCExtensionLocationIdentifier { + switch self { + case .launched: + return .appLaunch + case .foreground: + return .appDidComeToForeground + case .background: + return .appDidBecomeBackgrounded + case .backgroundFetch: + return .appBackgroundFetch + } + } + } + + static let shared = ScheduledTaskManager() + + private var state: State = .launched { + willSet { + if state != newValue { + scheduleTasks() + } + } + } + private static let lowBatteryThreshold : Float = 0.2 + + private var lowBatteryDetected = false { + willSet { + if self.lowBatteryDetected != newValue { + scheduleTasks() + } + } + } + + private var externalPowerConnected = false { + willSet { + if self.externalPowerConnected != newValue { + scheduleTasks() + } + } + } + + private var wifiDetected = false { + willSet { + if self.wifiDetected != newValue { + scheduleTasks() + } + } + } + + private var photoLibraryChangeDetected = false { + willSet { + if self.photoLibraryChangeDetected != newValue && newValue == true { + scheduleTasks() + } + } + } + + private var wifiMonitorQueue: DispatchQueue? + private var wifiMonitor : Any? + private var monitoringPhotoLibrary = false + + var considerLowBattery : Bool { + get { + return UIDevice.current.isBatteryMonitoringEnabled + } + + set { + UIDevice.current.isBatteryMonitoringEnabled = newValue + if newValue == true { + NotificationCenter.default.addObserver(self, selector: #selector(batteryLevelDidChange), name: UIDevice.batteryLevelDidChangeNotification, object: nil) + } else { + NotificationCenter.default.removeObserver(self, name: UIDevice.batteryLevelDidChangeNotification, object: nil) + lowBatteryDetected = false + } + } + } + + private override init() { + super.init() + } + + deinit { + NotificationCenter.default.removeObserver(self) + if #available(iOS 12, *) { + (wifiMonitor as? NWPathMonitor)?.cancel() + } + stopMonitoringPhotoLibraryChanges() + } + + func setup() { + // Monitor app states + NotificationCenter.default.addObserver(self, selector: #selector(applicationStateChange), name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(applicationStateChange), name: UIApplication.didBecomeActiveNotification, object: nil) + + // Monitor media upload settings changes + NotificationCenter.default.addObserver(self, selector: #selector(mediaUploadSettingsDidChange), name: UserDefaults.MediaUploadSettingsChangedNotification, object: nil) + + // In iOS12 or later, activate Wifi monitoring + if #available(iOS 12, *) { + wifiMonitorQueue = DispatchQueue(label: "com.owncloud.scheduled_task_mgr.wifi_monitor") + wifiMonitor = NWPathMonitor(requiredInterfaceType: .wifi) + (wifiMonitor as? NWPathMonitor)?.pathUpdateHandler = { [weak self] path in + // Use "inexpensive" WiFi only (not behind a cellular hot-spot) + self?.wifiDetected = (path.status == .satisfied && !path.isExpensive) + } + (wifiMonitor as? NWPathMonitor)?.start(queue: wifiMonitorQueue!) + } + + checkPowerState() + + startMonitoringPhotoLibraryChangesIfNecessary() + } + + // MARK: - Notifications handling + + @objc private func applicationStateChange(notificaton:Notification) { + switch notificaton.name { + case UIApplication.didBecomeActiveNotification: + state = .foreground + case UIApplication.didEnterBackgroundNotification: + state = .background + // TODO: Find a better way how to prevent multiple invocation of instant upload tasks + photoLibraryChangeDetected = false + default: + break + } + } + + @objc private func batteryLevelDidChange(notification:Notification) { + checkPowerState() + } + + @objc private func mediaUploadSettingsDidChange(notification:Notification) { + if shallMonitorPhotoLibraryChanges() { + startMonitoringPhotoLibraryChangesIfNecessary() + } else { + stopMonitoringPhotoLibraryChanges() + } + } + + // MARK: - Background fetching + + func backgroundFetch(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + self.state = .backgroundFetch + scheduleTasks(fetchCompletion: completionHandler) + } + + // MARK: - Private methods + + private func getCurrentContext() -> OCExtensionContext { + // Build a context + let location = OCExtensionLocation(ofType: .scheduledTask, identifier: self.state.locationIdentifier()) + + // Add requirements + var requirements = [String : Bool]() + if self.wifiDetected { + requirements[ScheduledTaskAction.FeatureKeys.runOnWifi] = true + } + if self.lowBatteryDetected { + requirements[ScheduledTaskAction.FeatureKeys.runOnLowBattery] = true + } + if self.externalPowerConnected { + requirements[ScheduledTaskAction.FeatureKeys.runOnExternalPower] = true + } + if self.photoLibraryChangeDetected { + requirements[ScheduledTaskAction.FeatureKeys.photoLibraryChanged] = true + } + + return OCExtensionContext(location: location, requirements: requirements, preferences: nil) + } + + private func scheduleTasks(fetchCompletion:((UIBackgroundFetchResult) -> Void)? = nil, completion:((_ scheduledTaskCount:Int) -> Void)? = nil) { + OnMainThread { + + let state = self.state + let context = self.getCurrentContext() + + // Find a task to run + if let matches = try? OCExtensionManager.shared.provideExtensions(for: context) { + var bgFetchedNewDataTasks = 0 + var bgFailedTasks = 0 + let bgFetchGroup = DispatchGroup() + let queue = DispatchQueue.global(qos: .background) + + for match in matches { + if let task = match.extension.provideObject(for: context) as? ScheduledTaskAction { + // Set completion handler for the task performing background fetch + if state == .backgroundFetch { + task.backgroundFetchCompletion = { result in + switch result { + case .newData : + bgFetchedNewDataTasks += 1 + case .failed: + bgFailedTasks += 1 + default: + break + } + bgFetchGroup.leave() + } + } + + let backgroundExecution = state == .background + if backgroundExecution { + task.runUntil = Date().addingTimeInterval(UIApplication.shared.backgroundTimeRemaining) + } + if state == .backgroundFetch { + bgFetchGroup.enter() + } + queue.async { + task.run(background: backgroundExecution) + } + } + } + + // Report background fetch result back to the OS + if state == .backgroundFetch { + bgFetchGroup.notify(queue: queue, execute: { + if bgFetchedNewDataTasks > 0 { + fetchCompletion?(.newData) + } else if bgFailedTasks > 0 { + fetchCompletion?(.failed) + } else { + fetchCompletion?(.noData) + } + }) + } + + completion?(matches.count) + } else { + completion?(0) + } + } + } + + private func checkPowerState() { + if UIDevice.current.batteryLevel >= 0 { + lowBatteryDetected = (UIDevice.current.batteryLevel <= ScheduledTaskManager.lowBatteryThreshold) + } + if UIDevice.current.batteryState != .unknown { + externalPowerConnected = (UIDevice.current.batteryState != .unplugged) + } + } +} + +extension ScheduledTaskManager : PHPhotoLibraryChangeObserver { + + // MARK: - PHPhotoLibraryChangeObserver + + func photoLibraryDidChange(_ changeInstance: PHChange) { + photoLibraryChangeDetected = true + } + + // MARK: - Helper methods + + private func shallMonitorPhotoLibraryChanges() -> Bool { + guard PHPhotoLibrary.authorizationStatus() == .authorized else { return false } + + guard let settings = OCAppIdentity.shared.userDefaults else { return false } + + guard settings.instantUploadVideosAfter != nil || settings.instantUploadPhotosAfter != nil else { return false } + + return true + } + + private func startMonitoringPhotoLibraryChangesIfNecessary() { + if !monitoringPhotoLibrary && shallMonitorPhotoLibraryChanges() { + PHPhotoLibrary.shared().register(self) + monitoringPhotoLibrary = true + } + } + + private func stopMonitoringPhotoLibraryChanges() { + if monitoringPhotoLibrary { + PHPhotoLibrary.shared().unregisterChangeObserver(self) + monitoringPhotoLibrary = false + } + } +} diff --git a/ownCloud/Tools/URL+Extensions.swift b/ownCloud/Tools/URL+Extensions.swift new file mode 100644 index 000000000..40b4cb5b8 --- /dev/null +++ b/ownCloud/Tools/URL+Extensions.swift @@ -0,0 +1,47 @@ +// +// URL+Extensions.swift +// ownCloud +// +// Created by Michael Neuwert on 06.08.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +import Foundation +import ownCloudSDK + +typealias UploadHandler = (OCItem?, Error?) -> Void + +extension URL { + func upload(with core:OCCore?, at rootItem:OCItem, importByCopy:Bool = false, placeholderHandler:UploadHandler? = nil, completionHandler:UploadHandler? = nil) -> Progress? { + let fileName = self.lastPathComponent + let importOptions : [OCCoreOption : Any] = [OCCoreOption.importByCopying : importByCopy, OCCoreOption.automaticConflictResolutionNameStyle : OCCoreDuplicateNameStyle.bracketed.rawValue] + + var progress:Progress? + + if core != nil { + progress = core?.importFileNamed(fileName, + at: rootItem, + from: self, + isSecurityScoped: false, + options: importOptions, + placeholderCompletionHandler: { (error, item) in + if error != nil { + Log.error("Error creating placeholder item for \(Log.mask(fileName)), error: \(error!.localizedDescription)") + } + placeholderHandler?(item, error) + + }, resultHandler: { (error, _, item, _) in + if error != nil { + Log.error("Error uploading \(Log.mask(fileName)) to \(Log.mask(rootItem.path)), error: \(error?.localizedDescription ?? "" )") + } else { + Log.debug("Success uploading \(Log.mask(fileName)) to \(Log.mask(rootItem.path))") + } + completionHandler?(item, error) + }) + } else { + completionHandler?(nil, NSError(ocError: .internal)) + } + + return progress + } +} diff --git a/ownCloud/UIKit Extensions/UIAlertViewController+SystemPermissions.swift b/ownCloud/UIKit Extensions/UIAlertViewController+SystemPermissions.swift new file mode 100644 index 000000000..03c7c2f4a --- /dev/null +++ b/ownCloud/UIKit Extensions/UIAlertViewController+SystemPermissions.swift @@ -0,0 +1,36 @@ +// +// UIAlertViewController+SystemPermissions.swift +// ownCloud +// +// Created by Michael Neuwert on 17.07.2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, 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 UIKit + +extension UIAlertController { + + class func alertControllerForPhotoLibraryAuthorizationInSettings() -> UIAlertController { + let alert = UIAlertController(title: "Missing permissions".localized, message: "This permission is needed to upload photos and videos from your photo library.".localized, preferredStyle: .alert) + + let settingAction = UIAlertAction(title: "Settings".localized, style: .default, handler: { _ in + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + }) + let notNowAction = UIAlertAction(title: "Not now".localized, style: .cancel) + + alert.addAction(settingAction) + alert.addAction(notNowAction) + + return alert + } +} diff --git a/ownCloudScreenshotsTests/SnapshotHelper.swift b/ownCloudScreenshotsTests/SnapshotHelper.swift index 83674c7d7..964e2ee9c 100644 --- a/ownCloudScreenshotsTests/SnapshotHelper.swift +++ b/ownCloudScreenshotsTests/SnapshotHelper.swift @@ -176,7 +176,7 @@ open class Snapshot: NSObject { let window = app.windows.firstMatch let screenshot = window.screenshot() guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } - + do { // The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices let regex = try NSRegularExpression(pattern: "Clone [0-1]+ of ") @@ -226,9 +226,11 @@ open class Snapshot: NSObject { guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { throw SnapshotError.cannotFindSimulatorHomeDirectory } + guard let homeDirUrl = URL(string: simulatorHostHome) else { throw SnapshotError.cannotAccessSimulatorHomeDirectory(simulatorHostHome) } + homeDir = URL(fileURLWithPath: homeDirUrl.path) #else throw SnapshotError.cannotRunOnPhysicalDevice