| /* |
| * Copyright (C) 2017 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #import "WebItemProviderPasteboard.h" |
| |
| #if ENABLE(DATA_INTERACTION) |
| |
| #import "UIKitSPI.h" |
| #import <Foundation/NSProgress.h> |
| #import <MobileCoreServices/MobileCoreServices.h> |
| #import <UIKit/UIColor.h> |
| #import <UIKit/UIImage.h> |
| #import <WebCore/FileSystemIOS.h> |
| #import <WebCore/Pasteboard.h> |
| #import <wtf/BlockPtr.h> |
| #import <wtf/OSObjectPtr.h> |
| #import <wtf/RetainPtr.h> |
| #import <wtf/SoftLinking.h> |
| |
| SOFT_LINK_FRAMEWORK(UIKit) |
| SOFT_LINK_CLASS(UIKit, UIColor) |
| SOFT_LINK_CLASS(UIKit, UIImage) |
| SOFT_LINK_CLASS(UIKit, UIItemProvider) |
| |
| // FIXME: Remove once +objectWithItemProviderData:typeIdentifier:error: is available in the public SDK. |
| @interface NSObject (Foundation_NSItemProvider_Staging) |
| + (id <NSItemProviderReading>)objectWithItemProviderData:(NSData *)data typeIdentifier:(NSString *)typeIdentifier error:(NSError **)outError; |
| - (id <NSItemProviderReading>)initWithItemProviderData:(NSData *)data typeIdentifier:(NSString *)typeIdentifier error:(NSError **)outError; |
| @end |
| |
| using namespace WebCore; |
| |
| typedef void(^ItemProviderDataLoadCompletionHandler)(NSData *, NSError *); |
| typedef NSDictionary<NSString *, NSURL *> TypeToFileURLMap; |
| |
| @interface WebItemProviderRegistrationInfo () |
| { |
| RetainPtr<id <UIItemProviderWriting>> _representingObject; |
| RetainPtr<NSString> _typeIdentifier; |
| RetainPtr<NSData> _data; |
| } |
| @end |
| |
| @implementation WebItemProviderRegistrationInfo |
| |
| - (instancetype)initWithRepresentingObject:(id <UIItemProviderWriting>)representingObject typeIdentifier:(NSString *)typeIdentifier data:(NSData *)data |
| { |
| if (representingObject) |
| ASSERT(!typeIdentifier && !data); |
| else |
| ASSERT(typeIdentifier && data); |
| |
| if (self = [super init]) { |
| _representingObject = representingObject; |
| _typeIdentifier = typeIdentifier; |
| _data = data; |
| } |
| return self; |
| } |
| |
| - (id <UIItemProviderWriting>)representingObject |
| { |
| return _representingObject.get(); |
| } |
| |
| - (NSString *)typeIdentifier |
| { |
| return _typeIdentifier.get(); |
| } |
| |
| - (NSData *)data |
| { |
| return _data.get(); |
| } |
| |
| @end |
| |
| @interface WebItemProviderRegistrationInfoList () |
| { |
| RetainPtr<NSMutableArray> _items; |
| } |
| @end |
| |
| @implementation WebItemProviderRegistrationInfoList |
| |
| - (instancetype)init |
| { |
| if (self = [super init]) { |
| _items = adoptNS([[NSMutableArray alloc] init]); |
| _estimatedDisplayedSize = CGSizeZero; |
| } |
| |
| return self; |
| } |
| |
| - (void)addData:(NSData *)data forType:(NSString *)typeIdentifier |
| { |
| [_items addObject:[[[WebItemProviderRegistrationInfo alloc] initWithRepresentingObject:nil typeIdentifier:typeIdentifier data:data] autorelease]]; |
| } |
| |
| - (void)addRepresentingObject:(id <UIItemProviderWriting>)object |
| { |
| ASSERT([object conformsToProtocol:@protocol(UIItemProviderWriting)]); |
| [_items addObject:[[[WebItemProviderRegistrationInfo alloc] initWithRepresentingObject:object typeIdentifier:nil data:nil] autorelease]]; |
| } |
| |
| - (NSUInteger)numberOfItems |
| { |
| return [_items count]; |
| } |
| |
| - (WebItemProviderRegistrationInfo *)itemAtIndex:(NSUInteger)index |
| { |
| if (index >= self.numberOfItems) |
| return nil; |
| |
| return [_items objectAtIndex:index]; |
| } |
| |
| - (void)enumerateItems:(void (^)(WebItemProviderRegistrationInfo *, NSUInteger))block |
| { |
| for (NSUInteger index = 0; index < self.numberOfItems; ++index) |
| block([self itemAtIndex:index], index); |
| } |
| |
| @end |
| |
| @interface WebItemProviderPasteboard () |
| |
| @property (nonatomic) NSInteger numberOfItems; |
| @property (nonatomic) NSInteger changeCount; |
| @property (nonatomic) NSInteger pendingOperationCount; |
| |
| @end |
| |
| @implementation WebItemProviderPasteboard { |
| // FIXME: These ivars should be refactored to be Vector<RetainPtr<Type>> instead of generic NSArrays. |
| RetainPtr<NSArray> _itemProviders; |
| RetainPtr<NSArray> _cachedTypeIdentifiers; |
| RetainPtr<NSArray> _typeToFileURLMaps; |
| RetainPtr<NSArray> _supportedTypeIdentifiers; |
| RetainPtr<NSArray> _registrationInfoLists; |
| } |
| |
| + (instancetype)sharedInstance |
| { |
| static WebItemProviderPasteboard *sharedPasteboard = nil; |
| static dispatch_once_t onceToken; |
| dispatch_once(&onceToken, ^() { |
| sharedPasteboard = [[WebItemProviderPasteboard alloc] init]; |
| }); |
| return sharedPasteboard; |
| } |
| |
| - (instancetype)init |
| { |
| if (self = [super init]) { |
| _itemProviders = adoptNS([[NSArray alloc] init]); |
| _changeCount = 0; |
| _pendingOperationCount = 0; |
| _typeToFileURLMaps = adoptNS([[NSArray alloc] init]); |
| _supportedTypeIdentifiers = nil; |
| _registrationInfoLists = nil; |
| } |
| return self; |
| } |
| |
| - (void)updateSupportedTypeIdentifiers:(NSArray<NSString *> *)types |
| { |
| _supportedTypeIdentifiers = types; |
| } |
| |
| - (NSArray<NSString *> *)pasteboardTypesByFidelityForItemAtIndex:(NSUInteger)index |
| { |
| return [self itemProviderAtIndex:index].registeredTypeIdentifiers ?: @[ ]; |
| } |
| |
| - (NSArray<NSString *> *)pasteboardTypes |
| { |
| if (_cachedTypeIdentifiers) |
| return _cachedTypeIdentifiers.get(); |
| |
| NSMutableSet<NSString *> *allTypes = [NSMutableSet set]; |
| NSMutableArray<NSString *> *allTypesInOrder = [NSMutableArray array]; |
| for (UIItemProvider *provider in _itemProviders.get()) { |
| for (NSString *typeIdentifier in provider.registeredTypeIdentifiers) { |
| if ([allTypes containsObject:typeIdentifier]) |
| continue; |
| |
| [allTypes addObject:typeIdentifier]; |
| [allTypesInOrder addObject:typeIdentifier]; |
| } |
| } |
| _cachedTypeIdentifiers = allTypesInOrder; |
| return _cachedTypeIdentifiers.get(); |
| } |
| |
| - (NSArray<__kindof NSItemProvider *> *)itemProviders |
| { |
| return _itemProviders.get(); |
| } |
| |
| - (void)setItemProviders:(NSArray<__kindof NSItemProvider *> *)itemProviders |
| { |
| itemProviders = itemProviders ?: [NSArray array]; |
| if (_itemProviders == itemProviders || [_itemProviders isEqualToArray:itemProviders]) |
| return; |
| |
| _itemProviders = itemProviders; |
| _changeCount++; |
| _cachedTypeIdentifiers = nil; |
| _registrationInfoLists = nil; |
| |
| NSMutableArray *typeToFileURLMaps = [NSMutableArray arrayWithCapacity:itemProviders.count]; |
| [itemProviders enumerateObjectsUsingBlock:[typeToFileURLMaps] (UIItemProvider *, NSUInteger, BOOL *) { |
| [typeToFileURLMaps addObject:@{ }]; |
| }]; |
| } |
| |
| - (NSInteger)numberOfItems |
| { |
| return [_itemProviders count]; |
| } |
| |
| - (void)setItemsUsingRegistrationInfoLists:(NSArray<WebItemProviderRegistrationInfoList *> *)itemLists |
| { |
| NSMutableArray *providers = [NSMutableArray array]; |
| for (WebItemProviderRegistrationInfoList *itemList in itemLists) { |
| if (!itemList.numberOfItems) |
| continue; |
| |
| auto itemProvider = adoptNS([allocUIItemProviderInstance() init]); |
| [itemList enumerateItems:[itemProvider] (WebItemProviderRegistrationInfo *item, NSUInteger) { |
| if (item.representingObject) { |
| [itemProvider registerObject:item.representingObject visibility:UIItemProviderRepresentationOptionsVisibilityAll]; |
| return; |
| } |
| |
| if (!item.typeIdentifier.length || !item.data.length) |
| return; |
| |
| RetainPtr<NSData> itemData = item.data; |
| [itemProvider registerDataRepresentationForTypeIdentifier:item.typeIdentifier visibility:UIItemProviderRepresentationOptionsVisibilityAll loadHandler:[itemData] (ItemProviderDataLoadCompletionHandler completionHandler) -> NSProgress * { |
| completionHandler(itemData.get(), nil); |
| return nil; |
| }]; |
| }]; |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| [itemProvider setEstimatedDisplayedSize:itemList.estimatedDisplayedSize]; |
| #pragma clang diagnostic pop |
| [itemProvider setSuggestedName:itemList.suggestedName]; |
| [providers addObject:itemProvider.get()]; |
| } |
| |
| self.itemProviders = providers; |
| _registrationInfoLists = itemLists; |
| } |
| |
| - (NSData *)_preLoadedDataConformingToType:(NSString *)typeIdentifier forItemProviderAtIndex:(NSUInteger)index |
| { |
| if ([_typeToFileURLMaps count] != [_itemProviders count]) { |
| ASSERT_NOT_REACHED(); |
| return nil; |
| } |
| |
| TypeToFileURLMap *typeToFileURLMap = [_typeToFileURLMaps objectAtIndex:index]; |
| for (NSString *loadedType in typeToFileURLMap) { |
| if (!UTTypeConformsTo((CFStringRef)loadedType, (CFStringRef)typeIdentifier)) |
| continue; |
| |
| // We've already loaded data relevant for this UTI type onto disk, so there's no need to ask the UIItemProvider for the same data again. |
| if (NSData *result = [NSData dataWithContentsOfURL:typeToFileURLMap[loadedType] options:NSDataReadingMappedIfSafe error:nil]) |
| return result; |
| } |
| return nil; |
| } |
| |
| - (NSArray *)dataForPasteboardType:(NSString *)pasteboardType inItemSet:(NSIndexSet *)itemSet |
| { |
| auto values = adoptNS([[NSMutableArray alloc] init]); |
| RetainPtr<WebItemProviderPasteboard> retainedSelf = self; |
| [itemSet enumerateIndexesUsingBlock:[retainedSelf, pasteboardType, values] (NSUInteger index, BOOL *) { |
| UIItemProvider *provider = [retainedSelf itemProviderAtIndex:index]; |
| if (!provider) |
| return; |
| |
| if (NSData *loadedData = [retainedSelf _preLoadedDataConformingToType:pasteboardType forItemProviderAtIndex:index]) |
| [values addObject:loadedData]; |
| }]; |
| return values.autorelease(); |
| } |
| |
| static NSArray<Class<UIItemProviderReading>> *allLoadableClasses() |
| { |
| return @[ [getUIColorClass() class], [getUIImageClass() class], [NSURL class], [NSString class], [NSAttributedString class] ]; |
| } |
| |
| static Class classForTypeIdentifier(NSString *typeIdentifier, NSString *&outTypeIdentifierToLoad) |
| { |
| outTypeIdentifierToLoad = typeIdentifier; |
| |
| // First, try to load a platform UIItemProviderReading-conformant object as-is. |
| for (Class<UIItemProviderReading> loadableClass in allLoadableClasses()) { |
| if ([[loadableClass readableTypeIdentifiersForItemProvider] containsObject:(NSString *)typeIdentifier]) |
| return loadableClass; |
| } |
| |
| // If we were unable to load any object, check if the given type identifier is still something |
| // WebKit knows how to handle. |
| if ([typeIdentifier isEqualToString:(NSString *)kUTTypeHTML]) { |
| // Load kUTTypeHTML as a plain text HTML string. |
| outTypeIdentifierToLoad = (NSString *)kUTTypePlainText; |
| return [NSString class]; |
| } |
| |
| return nil; |
| } |
| |
| - (NSArray *)valuesForPasteboardType:(NSString *)pasteboardType inItemSet:(NSIndexSet *)itemSet |
| { |
| auto values = adoptNS([[NSMutableArray alloc] init]); |
| RetainPtr<WebItemProviderPasteboard> retainedSelf = self; |
| [itemSet enumerateIndexesUsingBlock:[retainedSelf, pasteboardType, values] (NSUInteger index, BOOL *) { |
| UIItemProvider *provider = [retainedSelf itemProviderAtIndex:index]; |
| if (!provider) |
| return; |
| |
| NSString *typeIdentifierToLoad; |
| Class readableClass = classForTypeIdentifier(pasteboardType, typeIdentifierToLoad); |
| if (!readableClass) |
| return; |
| |
| NSData *preloadedData = [retainedSelf _preLoadedDataConformingToType:pasteboardType forItemProviderAtIndex:index]; |
| if (!preloadedData) |
| return; |
| |
| if ([readableClass respondsToSelector:@selector(objectWithItemProviderData:typeIdentifier:error:)]) { |
| if (id <NSItemProviderReading> readObject = [readableClass objectWithItemProviderData:preloadedData typeIdentifier:(NSString *)typeIdentifierToLoad error:nil]) |
| [values addObject:readObject]; |
| } else { |
| if (auto readObject = adoptNS([[readableClass alloc] initWithItemProviderData:preloadedData typeIdentifier:(NSString *)typeIdentifierToLoad error:nil])) |
| [values addObject:readObject.get()]; |
| } |
| }]; |
| |
| return values.autorelease(); |
| } |
| |
| - (NSInteger)changeCount |
| { |
| return _changeCount; |
| } |
| |
| - (NSArray<NSURL *> *)fileURLsForDataInteraction |
| { |
| NSMutableArray<NSURL *> *fileURLs = [NSMutableArray array]; |
| for (TypeToFileURLMap *typeToFileURLMap in _typeToFileURLMaps.get()) |
| [fileURLs addObjectsFromArray:[typeToFileURLMap allValues]]; |
| return fileURLs; |
| } |
| |
| static BOOL typeConformsToTypes(NSString *type, NSArray *conformsToTypes) |
| { |
| // A type is considered appropriate to load if it conforms to one or more supported types. |
| for (NSString *conformsToType in conformsToTypes) { |
| if (UTTypeConformsTo((CFStringRef)type, (CFStringRef)conformsToType)) |
| return YES; |
| } |
| return NO; |
| } |
| |
| - (NSInteger)numberOfFiles |
| { |
| NSArray *supportedFileTypes = Pasteboard::supportedFileUploadPasteboardTypes(); |
| NSInteger numberOfFiles = 0; |
| for (UIItemProvider *itemProvider in _itemProviders.get()) { |
| for (NSString *identifier in itemProvider.registeredTypeIdentifiers) { |
| if (!typeConformsToTypes(identifier, supportedFileTypes)) |
| continue; |
| ++numberOfFiles; |
| break; |
| } |
| } |
| return numberOfFiles; |
| } |
| |
| static NSURL *temporaryFileURLForDataInteractionContent(NSURL *url, NSString *suggestedName) |
| { |
| static NSString *defaultDataInteractionFileName = @"file"; |
| static NSString *dataInteractionDirectoryPrefix = @"data-interaction"; |
| if (!url) |
| return nil; |
| |
| NSString *temporaryDataInteractionDirectory = WebCore::createTemporaryDirectory(dataInteractionDirectoryPrefix); |
| if (!temporaryDataInteractionDirectory) |
| return nil; |
| |
| suggestedName = suggestedName ?: defaultDataInteractionFileName; |
| if (![suggestedName containsString:@"."]) |
| suggestedName = [suggestedName stringByAppendingPathExtension:url.pathExtension]; |
| |
| return [NSURL fileURLWithPath:[temporaryDataInteractionDirectory stringByAppendingPathComponent:suggestedName ?: url.lastPathComponent]]; |
| } |
| |
| - (NSString *)typeIdentifierToLoadForRegisteredTypeIdentfiers:(NSArray<NSString *> *)registeredTypeIdentifiers |
| { |
| NSString *highestFidelityContentType = nil; |
| for (NSString *registeredTypeIdentifier in registeredTypeIdentifiers) { |
| if (typeConformsToTypes(registeredTypeIdentifier, _supportedTypeIdentifiers.get())) |
| return registeredTypeIdentifier; |
| |
| if (!highestFidelityContentType && UTTypeConformsTo((CFStringRef)registeredTypeIdentifier, kUTTypeContent)) |
| highestFidelityContentType = registeredTypeIdentifier; |
| } |
| return highestFidelityContentType; |
| } |
| |
| - (void)doAfterLoadingProvidedContentIntoFileURLs:(WebItemProviderFileLoadBlock)action |
| { |
| [self doAfterLoadingProvidedContentIntoFileURLs:action synchronousTimeout:0]; |
| } |
| |
| - (void)doAfterLoadingProvidedContentIntoFileURLs:(WebItemProviderFileLoadBlock)action synchronousTimeout:(NSTimeInterval)synchronousTimeout |
| { |
| auto changeCountBeforeLoading = _changeCount; |
| auto typeToFileURLMaps = adoptNS([[NSMutableArray alloc] initWithCapacity:[_itemProviders count]]); |
| |
| // First, figure out which item providers we want to try and load files from. |
| auto itemProvidersToLoad = adoptNS([[NSMutableArray alloc] init]); |
| auto typeIdentifiersToLoad = adoptNS([[NSMutableArray alloc] init]); |
| auto indicesOfitemProvidersToLoad = adoptNS([[NSMutableArray alloc] init]); |
| RetainPtr<WebItemProviderPasteboard> protectedSelf = self; |
| [_itemProviders enumerateObjectsUsingBlock:[protectedSelf, itemProvidersToLoad, typeIdentifiersToLoad, indicesOfitemProvidersToLoad, typeToFileURLMaps] (UIItemProvider *itemProvider, NSUInteger index, BOOL *) { |
| NSString *typeIdentifierToLoad = [protectedSelf typeIdentifierToLoadForRegisteredTypeIdentfiers:itemProvider.registeredTypeIdentifiers]; |
| if (typeIdentifierToLoad) { |
| [itemProvidersToLoad addObject:itemProvider]; |
| [typeIdentifiersToLoad addObject:typeIdentifierToLoad]; |
| [indicesOfitemProvidersToLoad addObject:@(index)]; |
| } |
| [typeToFileURLMaps addObject:@{ }]; |
| }]; |
| |
| if (![itemProvidersToLoad count]) { |
| action(@[ ]); |
| return; |
| } |
| |
| auto setFileURLsLock = adoptNS([[NSLock alloc] init]); |
| auto synchronousFileLoadingGroup = adoptOSObject(dispatch_group_create()); |
| auto fileLoadingGroup = adoptOSObject(dispatch_group_create()); |
| for (NSUInteger index = 0; index < [itemProvidersToLoad count]; ++index) { |
| RetainPtr<UIItemProvider> itemProvider = [itemProvidersToLoad objectAtIndex:index]; |
| RetainPtr<NSString> typeIdentifier = [typeIdentifiersToLoad objectAtIndex:index]; |
| NSUInteger indexInItemProviderArray = [[indicesOfitemProvidersToLoad objectAtIndex:index] unsignedIntegerValue]; |
| RetainPtr<NSString> suggestedName = [itemProvider suggestedName]; |
| dispatch_group_enter(fileLoadingGroup.get()); |
| dispatch_group_enter(synchronousFileLoadingGroup.get()); |
| [itemProvider loadFileRepresentationForTypeIdentifier:typeIdentifier.get() completionHandler:[synchronousFileLoadingGroup, setFileURLsLock, indexInItemProviderArray, suggestedName, typeIdentifier, typeToFileURLMaps, fileLoadingGroup] (NSURL *url, NSError *error) { |
| // After executing this completion block, UIKit removes the file at the given URL. However, we need this data to persist longer for the web content process. |
| // To address this, we hard link the given URL to a new temporary file in the temporary directory. This follows the same flow as regular file upload, in |
| // WKFileUploadPanel.mm. The temporary files are cleaned up by the system at a later time. |
| RetainPtr<NSURL> destinationURL = temporaryFileURLForDataInteractionContent(url, suggestedName.get()); |
| if (destinationURL && !error && [[NSFileManager defaultManager] linkItemAtURL:url toURL:destinationURL.get() error:nil]) { |
| [setFileURLsLock lock]; |
| [typeToFileURLMaps setObject:[NSDictionary dictionaryWithObject:destinationURL.get() forKey:typeIdentifier.get()] atIndexedSubscript:indexInItemProviderArray]; |
| [setFileURLsLock unlock]; |
| } |
| dispatch_group_leave(fileLoadingGroup.get()); |
| dispatch_group_leave(synchronousFileLoadingGroup.get()); |
| }]; |
| } |
| |
| RetainPtr<WebItemProviderPasteboard> retainedSelf = self; |
| auto itemLoadCompletion = [retainedSelf, synchronousFileLoadingGroup, fileLoadingGroup, typeToFileURLMaps, completionBlock = makeBlockPtr(action), changeCountBeforeLoading] { |
| if (changeCountBeforeLoading == retainedSelf->_changeCount) |
| retainedSelf->_typeToFileURLMaps = typeToFileURLMaps; |
| |
| completionBlock([retainedSelf fileURLsForDataInteraction]); |
| }; |
| |
| if (synchronousTimeout > 0 && !dispatch_group_wait(synchronousFileLoadingGroup.get(), dispatch_time(DISPATCH_TIME_NOW, synchronousTimeout * NSEC_PER_SEC))) { |
| itemLoadCompletion(); |
| return; |
| } |
| |
| dispatch_group_notify(fileLoadingGroup.get(), dispatch_get_main_queue(), itemLoadCompletion); |
| } |
| |
| - (WebItemProviderRegistrationInfoList *)registrationInfoAtIndex:(NSUInteger)index |
| { |
| return index < [_registrationInfoLists count] ? [_registrationInfoLists objectAtIndex:index] : nil; |
| } |
| |
| - (UIItemProvider *)itemProviderAtIndex:(NSUInteger)index |
| { |
| return index < [_itemProviders count] ? [_itemProviders objectAtIndex:index] : nil; |
| } |
| |
| - (BOOL)hasPendingOperation |
| { |
| return _pendingOperationCount; |
| } |
| |
| - (void)incrementPendingOperationCount |
| { |
| _pendingOperationCount++; |
| } |
| |
| - (void)decrementPendingOperationCount |
| { |
| _pendingOperationCount--; |
| } |
| |
| - (void)enumerateItemProvidersWithBlock:(void (^)(UIItemProvider *itemProvider, NSUInteger index, BOOL *stop))block |
| { |
| [_itemProviders enumerateObjectsUsingBlock:block]; |
| } |
| |
| @end |
| |
| #endif // ENABLE(DATA_INTERACTION) |