// // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation import Photos import PromiseKit protocol PhotoLibraryDelegate: class { func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) } class PhotoMediaSize { var thumbnailSize: CGSize init() { self.thumbnailSize = .zero } init(thumbnailSize: CGSize) { self.thumbnailSize = thumbnailSize } } class PhotoPickerAssetItem: PhotoGridItem { let asset: PHAsset let album: PhotoCollectionContents let photoMediaSize: PhotoMediaSize init(asset: PHAsset, album: PhotoCollectionContents, photoMediaSize: PhotoMediaSize) { self.asset = asset self.album = album self.photoMediaSize = photoMediaSize } // MARK: PhotoGridItem var type: PhotoGridItemType { if asset.mediaType == .video { return .video } // TODO show GIF badge? return .photo } func asyncThumbnail(completion: @escaping (UIImage?) -> Void) -> UIImage? { album.requestThumbnail(for: self.asset, thumbnailSize: photoMediaSize.thumbnailSize) { image, _ in completion(image) } return nil } } class PhotoCollectionContents { let fetchResult: PHFetchResult let localizedTitle: String? enum PhotoLibraryError: Error { case assertionError(description: String) case unsupportedMediaType } init(fetchResult: PHFetchResult, localizedTitle: String?) { self.fetchResult = fetchResult self.localizedTitle = localizedTitle } var count: Int { return fetchResult.count } private let imageManager = PHCachingImageManager() func asset(at index: Int) -> PHAsset { return fetchResult.object(at: index) } func assetItem(at index: Int, photoMediaSize: PhotoMediaSize) -> PhotoPickerAssetItem { let mediaAsset = asset(at: index) return PhotoPickerAssetItem(asset: mediaAsset, album: self, photoMediaSize: photoMediaSize) } // MARK: ImageManager func requestThumbnail(for asset: PHAsset, thumbnailSize: CGSize, resultHandler: @escaping (UIImage?, [AnyHashable: Any]?) -> Void) { _ = imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: resultHandler) } private func requestImageDataSource(for asset: PHAsset) -> Promise<(dataSource: DataSource, dataUTI: String)> { return Promise { resolver in _ = imageManager.requestImageData(for: asset, options: nil) { imageData, dataUTI, _, _ in guard let imageData = imageData else { resolver.reject(PhotoLibraryError.assertionError(description: "imageData was unexpectedly nil")) return } guard let dataUTI = dataUTI else { resolver.reject(PhotoLibraryError.assertionError(description: "dataUTI was unexpectedly nil")) return } guard let dataSource = DataSourceValue.dataSource(with: imageData, utiType: dataUTI) else { resolver.reject(PhotoLibraryError.assertionError(description: "dataSource was unexpectedly nil")) return } resolver.fulfill((dataSource: dataSource, dataUTI: dataUTI)) } } } private func requestVideoDataSource(for asset: PHAsset) -> Promise<(dataSource: DataSource, dataUTI: String)> { return Promise { resolver in _ = imageManager.requestExportSession(forVideo: asset, options: nil, exportPreset: AVAssetExportPresetMediumQuality) { exportSession, _ in guard let exportSession = exportSession else { resolver.reject(PhotoLibraryError.assertionError(description: "exportSession was unexpectedly nil")) return } exportSession.outputFileType = AVFileType.mp4 exportSession.metadataItemFilter = AVMetadataItemFilter.forSharing() let exportPath = OWSFileSystem.temporaryFilePath(withFileExtension: "mp4") let exportURL = URL(fileURLWithPath: exportPath) exportSession.outputURL = exportURL Logger.debug("starting video export") exportSession.exportAsynchronously { Logger.debug("Completed video export") guard let dataSource = DataSourcePath.dataSource(with: exportURL, shouldDeleteOnDeallocation: true) else { resolver.reject(PhotoLibraryError.assertionError(description: "Failed to build data source for exported video URL")) return } resolver.fulfill((dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String)) } } } } func outgoingAttachment(for asset: PHAsset) -> Promise { switch asset.mediaType { case .image: return requestImageDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium) } case .video: return requestVideoDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI) } default: return Promise(error: PhotoLibraryError.unsupportedMediaType) } } } class PhotoCollection { private let collection: PHAssetCollection init(collection: PHAssetCollection) { self.collection = collection } func localizedTitle() -> String { guard let localizedTitle = collection.localizedTitle?.stripped, localizedTitle.count > 0 else { return NSLocalizedString("PHOTO_PICKER_UNNAMED_COLLECTION", comment: "label for system photo collections which have no name.") } return localizedTitle } func contents() -> PhotoCollectionContents { let options = PHFetchOptions() options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] let fetchResult = PHAsset.fetchAssets(in: collection, options: options) return PhotoCollectionContents(fetchResult: fetchResult, localizedTitle: localizedTitle()) } } class PhotoCollections { let collections: [PhotoCollection] init(collections: [PhotoCollection]) { self.collections = collections } var count: Int { return collections.count } func collection(at index: Int) -> PhotoCollection { return collections[index] } } class PhotoLibrary: NSObject, PHPhotoLibraryChangeObserver { final class WeakDelegate { weak var delegate: PhotoLibraryDelegate? init(_ value: PhotoLibraryDelegate) { delegate = value } } var delegates = [WeakDelegate]() public func add(delegate: PhotoLibraryDelegate) { delegates.append(WeakDelegate(delegate)) } var assetCollection: PHAssetCollection! func photoLibraryDidChange(_ changeInstance: PHChange) { DispatchQueue.main.async { for weakDelegate in self.delegates { weakDelegate.delegate?.photoLibraryDidChange(self) } } } override init() { super.init() PHPhotoLibrary.shared().register(self) } deinit { PHPhotoLibrary.shared().unregisterChangeObserver(self) } func defaultPhotoCollection() -> PhotoCollection { guard let photoCollection = allPhotoCollections().collections.first else { owsFail("Could not locate Camera Roll.") } return photoCollection } func allPhotoCollections() -> PhotoCollections { var collections = [PhotoCollection]() var collectionIds = Set() let processPHCollection: (PHCollection) -> Void = { (collection) in // De-duplicate by id. let collectionId = collection.localIdentifier guard !collectionIds.contains(collectionId) else { return } collectionIds.insert(collectionId) guard let assetCollection = collection as? PHAssetCollection else { owsFailDebug("Asset collection has unexpected type: \(type(of: collection))") return } let photoCollection = PhotoCollection(collection: assetCollection) // Hide empty collections. guard photoCollection.contents().count > 0 else { return } collections.append(photoCollection) } let processPHAssetCollections: (PHFetchResult) -> Void = { (fetchResult) in for index in 0..) -> Void = { (fetchResult) in for index in 0..