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