[iOS DnD] Upstream iOS drag and drop implementation into OpenSource WebKit
[WebKit.git] / Tools / TestWebKitAPI / ios / DataInteractionSimulator.mm
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #include "DataInteractionSimulator.h"
28
29 #if ENABLE(DATA_INTERACTION)
30
31 #import "InstanceMethodSwizzler.h"
32 #import "PlatformUtilities.h"
33 #import <UIKit/UIDragInteraction.h>
34 #import <UIKit/UIDragItem.h>
35 #import <UIKit/UIDragSession.h>
36 #import <UIKit/UIDragging.h>
37 #import <UIKit/UIItemProvider_Private.h>
38 #import <WebCore/SoftLinking.h>
39 #import <WebKit/WKWebViewPrivate.h>
40 #import <WebKit/_WKFocusedElementInfo.h>
41 #import <WebKit/_WKFormInputSession.h>
42 #import <wtf/RetainPtr.h>
43
44 SOFT_LINK_FRAMEWORK(UIKit)
45 SOFT_LINK(UIKit, UIApplicationInstantiateSingleton, void, (Class singletonClass), (singletonClass))
46
47 using namespace TestWebKitAPI;
48
49 @interface MockDragDropSession : NSObject <UIDragDropSession> {
50 @private
51     RetainPtr<NSArray> _mockItems;
52     RetainPtr<UIWindow> _window;
53 }
54 @property (nonatomic) CGPoint mockLocationInWindow;
55 @end
56
57 @implementation MockDragDropSession
58
59 - (instancetype)initWithItems:(NSArray <UIDragItem *>*)items location:(CGPoint)locationInWindow window:(UIWindow *)window
60 {
61     if (self = [super init]) {
62         _mockItems = items;
63         _mockLocationInWindow = locationInWindow;
64         _window = window;
65     }
66     return self;
67 }
68
69 - (BOOL)allowsMoveOperation
70 {
71     return YES;
72 }
73
74 - (BOOL)isRestrictedToDraggingApplication
75 {
76     return NO;
77 }
78
79 - (BOOL)hasItemsConformingToTypeIdentifiers:(NSArray<NSString *> *)typeIdentifiers
80 {
81     for (NSString *typeIdentifier in typeIdentifiers) {
82         BOOL hasItemConformingToType = NO;
83         for (UIDragItem *item in self.items)
84             hasItemConformingToType |= [[item.itemProvider registeredTypeIdentifiers] containsObject:typeIdentifier];
85         if (!hasItemConformingToType)
86             return NO;
87     }
88     return YES;
89 }
90
91 - (BOOL)canLoadObjectsOfClass:(Class<UIItemProviderReading>)aClass
92 {
93     for (UIDragItem *item in self.items) {
94         if ([item.itemProvider canLoadObjectOfClass:aClass])
95             return YES;
96     }
97     return NO;
98 }
99
100 - (BOOL)canLoadObjectsOfClasses:(NSArray<Class<UIItemProviderReading>> *)classes
101 {
102     for (Class<UIItemProviderReading> aClass in classes) {
103         BOOL canLoad = NO;
104         for (UIDragItem *item in self.items)
105             canLoad |= [item.itemProvider canLoadObjectOfClass:aClass];
106         if (!canLoad)
107             return NO;
108     }
109     return YES;
110 }
111
112 - (NSArray<UIDragItem *> *)items
113 {
114     return _mockItems.get();
115 }
116
117 - (void)setItems:(NSArray<UIDragItem *> *)items
118 {
119     _mockItems = items;
120 }
121
122 - (CGPoint)locationInView:(UIView *)view
123 {
124     return [_window convertPoint:_mockLocationInWindow toView:view];
125 }
126
127 @end
128
129 NSString * const DataInteractionEnterEventName = @"dragenter";
130 NSString * const DataInteractionOverEventName = @"dragover";
131 NSString * const DataInteractionPerformOperationEventName = @"drop";
132 NSString * const DataInteractionLeaveEventName = @"dragleave";
133 NSString * const DataInteractionStartEventName = @"dragstart";
134
135 @interface MockDataOperationSession : MockDragDropSession <UIDropSession>
136 @property (nonatomic, strong) id localContext;
137 @end
138
139 @implementation MockDataOperationSession
140
141 - (instancetype)initWithProviders:(NSArray<UIItemProvider *> *)providers location:(CGPoint)locationInWindow window:(UIWindow *)window
142 {
143     auto items = adoptNS([[NSMutableArray alloc] init]);
144     for (UIItemProvider *itemProvider in providers)
145         [items addObject:[[[UIDragItem alloc] initWithItemProvider:itemProvider] autorelease]];
146
147     return [super initWithItems:items.get() location:locationInWindow window:window];
148 }
149
150 - (UIDraggingSession *)session
151 {
152     return nil;
153 }
154
155 - (BOOL)isLocal
156 {
157     return YES;
158 }
159
160 - (NSProgress *)progress
161 {
162     return [NSProgress discreteProgressWithTotalUnitCount:100];
163 }
164
165 - (void)setProgressIndicatorStyle:(UIDropSessionProgressIndicatorStyle)progressIndicatorStyle
166 {
167 }
168
169 - (UIDropSessionProgressIndicatorStyle)progressIndicatorStyle
170 {
171     return UIDropSessionProgressIndicatorStyleNone;
172 }
173
174 - (NSUInteger)operationMask
175 {
176     return 0;
177 }
178
179 - (id <UIDragSession>)localDragSession
180 {
181     return nil;
182 }
183
184 - (BOOL)hasItemsConformingToTypeIdentifier:(NSString *)typeIdentifier
185 {
186     ASSERT_NOT_REACHED();
187     return NO;
188 }
189
190 - (BOOL)canCreateItemsOfClass:(Class<UIItemProviderReading>)aClass
191 {
192     ASSERT_NOT_REACHED();
193     return NO;
194 }
195
196 - (NSProgress *)loadObjectsOfClass:(Class<NSItemProviderReading>)aClass completion:(void(^)(NSArray<__kindof id <NSItemProviderReading>> *objects))completion
197 {
198     ASSERT_NOT_REACHED();
199     return nil;
200 }
201
202 @end
203
204 @interface MockDataInteractionSession : MockDragDropSession <UIDragSession>
205 @property (nonatomic, strong) id localContext;
206 @property (nonatomic, strong) id context;
207 @end
208
209 @implementation MockDataInteractionSession
210
211 - (instancetype)initWithWindow:(UIWindow *)window
212 {
213     return [super initWithItems:@[ ] location:CGPointZero window:window];
214 }
215
216 - (NSUInteger)localOperationMask
217 {
218     ASSERT_NOT_REACHED();
219     return 0;
220 }
221
222 - (NSUInteger)externalOperationMask
223 {
224     ASSERT_NOT_REACHED();
225     return 0;
226 }
227
228 - (id)session
229 {
230     return nil;
231 }
232
233 @end
234
235 static double progressIncrementStep = 0.033;
236 static double progressTimeStep = 0.016;
237 static NSString *TestWebKitAPISimulateCancelAllTouchesNotificationName = @"TestWebKitAPISimulateCancelAllTouchesNotificationName";
238
239 static NSArray *dataInteractionEventNames()
240 {
241     static NSArray *eventNames = nil;
242     static dispatch_once_t onceToken;
243     dispatch_once(&onceToken, ^() {
244         eventNames = @[ DataInteractionEnterEventName, DataInteractionOverEventName, DataInteractionPerformOperationEventName, DataInteractionLeaveEventName, DataInteractionStartEventName ];
245     });
246     return eventNames;
247 }
248
249 @interface DataInteractionSimulatorApplication : UIApplication
250 @end
251
252 @implementation DataInteractionSimulatorApplication
253 - (void)_cancelAllTouches
254 {
255     [[NSNotificationCenter defaultCenter] postNotificationName:TestWebKitAPISimulateCancelAllTouchesNotificationName object:nil];
256 }
257 @end
258
259 @implementation DataInteractionSimulator
260
261 - (instancetype)initWithWebView:(TestWKWebView *)webView
262 {
263     if (self = [super init]) {
264         _webView = webView;
265         _shouldEnsureUIApplication = NO;
266         _isDoneWaitingForInputSession = true;
267         [_webView setUIDelegate:self];
268         [_webView _setInputDelegate:self];
269     }
270     return self;
271 }
272
273 - (void)dealloc
274 {
275     if ([_webView UIDelegate] == self)
276         [_webView setUIDelegate:nil];
277
278     if ([_webView _inputDelegate] == self)
279         [_webView _setInputDelegate:nil];
280
281     [super dealloc];
282 }
283
284 - (void)_resetSimulatedState
285 {
286     _phase = DataInteractionBeginning;
287     _currentProgress = 0;
288     _isDoneWithCurrentRun = false;
289     _observedEventNames = adoptNS([[NSMutableArray alloc] init]);
290     _finalSelectionRects = @[ ];
291     _dataInteractionSession = nil;
292     _dataOperationSession = nil;
293     _shouldPerformOperation = NO;
294 }
295
296 - (NSArray *)observedEventNames
297 {
298     return _observedEventNames.get();
299 }
300
301 - (void)simulateAllTouchesCanceled:(NSNotification *)notification
302 {
303     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_advanceProgress) object:nil];
304     _phase = DataInteractionCancelled;
305     _currentProgress = 1;
306     _isDoneWithCurrentRun = true;
307     if (_dataInteractionSession)
308         [_webView _simulateDataInteractionSessionDidEnd:_dataInteractionSession.get()];
309 }
310
311 - (void)runFrom:(CGPoint)startLocation to:(CGPoint)endLocation
312 {
313     NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
314     [defaultCenter addObserver:self selector:@selector(simulateAllTouchesCanceled:) name:TestWebKitAPISimulateCancelAllTouchesNotificationName object:nil];
315
316     if (_shouldEnsureUIApplication)
317         UIApplicationInstantiateSingleton([DataInteractionSimulatorApplication class]);
318
319     [self _resetSimulatedState];
320
321     RetainPtr<DataInteractionSimulator> strongSelf = self;
322     for (NSString *eventName in dataInteractionEventNames()) {
323         DataInteractionSimulator *weakSelf = strongSelf.get();
324         [weakSelf->_webView performAfterReceivingMessage:eventName action:^() {
325             [weakSelf->_observedEventNames addObject:eventName];
326         }];
327     }
328
329     _startLocation = startLocation;
330     _endLocation = endLocation;
331
332     if (self.externalItemProviders.count) {
333         _dataOperationSession = adoptNS([[MockDataOperationSession alloc] initWithProviders:self.externalItemProviders location:_startLocation window:[_webView window]]);
334         _phase = DataInteractionBegan;
335         [self _advanceProgress];
336     } else {
337         _dataInteractionSession = adoptNS([[MockDataInteractionSession alloc] initWithWindow:[_webView window]]);
338         [_dataInteractionSession setMockLocationInWindow:_startLocation];
339         [_webView _simulatePrepareForDataInteractionSession:_dataInteractionSession.get() completion:^() {
340             DataInteractionSimulator *weakSelf = strongSelf.get();
341             if (weakSelf->_phase == DataInteractionCancelled)
342                 return;
343
344             weakSelf->_phase = DataInteractionBeginning;
345             [weakSelf _advanceProgress];
346         }];
347     }
348
349     Util::run(&_isDoneWithCurrentRun);
350     [_webView clearMessageHandlers:dataInteractionEventNames()];
351     _finalSelectionRects = [_webView selectionRectsAfterPresentationUpdate];
352
353     [defaultCenter removeObserver:self];
354 }
355
356 - (NSArray *)finalSelectionRects
357 {
358     return _finalSelectionRects.get();
359 }
360
361 - (void)_concludeDataInteractionAndPerformOperationIfNecessary
362 {
363     if (_shouldPerformOperation) {
364         [_webView _simulateDataInteractionPerformOperation:_dataOperationSession.get()];
365         _phase = DataInteractionPerforming;
366     } else {
367         _isDoneWithCurrentRun = YES;
368         _phase = DataInteractionCancelled;
369     }
370
371     [_webView _simulateDataInteractionEnded:_dataOperationSession.get()];
372
373     if (_dataInteractionSession)
374         [_webView _simulateDataInteractionSessionDidEnd:_dataInteractionSession.get()];
375 }
376
377 - (void)_advanceProgress
378 {
379     _currentProgress += progressIncrementStep;
380     CGPoint locationInWindow = self._currentLocation;
381     [_dataInteractionSession setMockLocationInWindow:locationInWindow];
382     [_dataOperationSession setMockLocationInWindow:locationInWindow];
383
384     if (_currentProgress >= 1) {
385         _currentProgress = 1;
386         [self _concludeDataInteractionAndPerformOperationIfNecessary];
387         return;
388     }
389
390     switch (_phase) {
391     case DataInteractionBeginning: {
392         NSMutableArray<UIItemProvider *> *itemProviders = [NSMutableArray array];
393         NSArray *items = [_webView _simulatedItemsForSession:_dataInteractionSession.get()];
394         if (!items.count) {
395             _phase = DataInteractionCancelled;
396             _currentProgress = 1;
397             _isDoneWithCurrentRun = true;
398             return;
399         }
400
401         for (UIDragItem *item in items)
402             [itemProviders addObject:item.itemProvider];
403
404         _dataOperationSession = adoptNS([[MockDataOperationSession alloc] initWithProviders:itemProviders location:self._currentLocation window:[_webView window]]);
405         [_dataInteractionSession setItems:items];
406         _sourceItemProviders = itemProviders;
407         if (self.showCustomActionSheetBlock) {
408             // Defer progress until the custom action sheet is dismissed.
409             return;
410         }
411
412         [_webView _simulateWillBeginDataInteractionWithSession:_dataInteractionSession.get()];
413
414         RetainPtr<WKWebView> retainedWebView = _webView;
415         dispatch_async(dispatch_get_main_queue(), ^() {
416             [retainedWebView resignFirstResponder];
417         });
418
419         _phase = DataInteractionBegan;
420         break;
421     }
422     case DataInteractionBegan:
423         [_webView _simulateDataInteractionEntered:_dataOperationSession.get()];
424         _phase = DataInteractionEntered;
425         break;
426     case DataInteractionEntered:
427         _shouldPerformOperation = [_webView _simulateDataInteractionUpdated:_dataOperationSession.get()];
428         break;
429     default:
430         break;
431     }
432
433     [self _scheduleAdvanceProgress];
434 }
435
436 - (CGPoint)_currentLocation
437 {
438     CGFloat distanceX = _endLocation.x - _startLocation.x;
439     CGFloat distanceY = _endLocation.y - _startLocation.y;
440     return CGPointMake(_startLocation.x + _currentProgress * distanceX, _startLocation.y + _currentProgress * distanceY);
441 }
442
443 - (void)_scheduleAdvanceProgress
444 {
445     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_advanceProgress) object:nil];
446     [self performSelector:@selector(_advanceProgress) withObject:nil afterDelay:progressTimeStep];
447 }
448
449 - (NSArray *)sourceItemProviders
450 {
451     return _sourceItemProviders.get();
452 }
453
454 - (NSArray *)externalItemProviders
455 {
456     return _externalItemProviders.get();
457 }
458
459 - (void)setExternalItemProviders:(NSArray *)externalItemProviders
460 {
461     _externalItemProviders = adoptNS([externalItemProviders copy]);
462 }
463
464 - (DataInteractionPhase)phase
465 {
466     return _phase;
467 }
468
469 - (void)waitForInputSession
470 {
471     _isDoneWaitingForInputSession = false;
472
473     // Waiting for an input session implies that we should allow input sessions to begin.
474     self.allowsFocusToStartInputSession = YES;
475
476     Util::run(&_isDoneWaitingForInputSession);
477 }
478
479 #pragma mark - WKUIDelegatePrivate
480
481 - (void)_webView:(WKWebView *)webView dataInteractionOperationWasHandled:(BOOL)handled forSession:(id)session itemProviders:(NSArray<UIItemProvider *> *)itemProviders
482 {
483     _isDoneWithCurrentRun = true;
484
485     if (self.dataInteractionOperationCompletionBlock)
486         self.dataInteractionOperationCompletionBlock(handled, itemProviders);
487 }
488
489 - (NSUInteger)_webView:(WKWebView *)webView willUpdateDataInteractionOperationToOperation:(NSUInteger)operation forSession:(id)session
490 {
491     return self.overrideDataInteractionOperationBlock ? self.overrideDataInteractionOperationBlock(operation, session) : operation;
492 }
493
494 - (NSArray *)_webView:(WKWebView *)webView adjustedDataInteractionItemProvidersForItemProvider:(UIItemProvider *)itemProvider representingObjects:(NSArray *)representingObjects additionalData:(NSDictionary *)additionalData
495 {
496     return self.convertItemProvidersBlock ? self.convertItemProvidersBlock(itemProvider, representingObjects, additionalData) : @[ itemProvider ];
497 }
498
499 - (BOOL)_webView:(WKWebView *)webView showCustomSheetForElement:(_WKActivatedElementInfo *)element
500 {
501     if (!self.showCustomActionSheetBlock)
502         return NO;
503
504     RetainPtr<DataInteractionSimulator> strongSelf = self;
505     dispatch_async(dispatch_get_main_queue(), ^() {
506         DataInteractionSimulator *weakSelf = strongSelf.get();
507         [weakSelf->_webView _simulateWillBeginDataInteractionWithSession:weakSelf->_dataInteractionSession.get()];
508         weakSelf->_phase = DataInteractionBegan;
509         [weakSelf _scheduleAdvanceProgress];
510     });
511
512     return self.showCustomActionSheetBlock(element);
513 }
514
515 - (NSArray<UIDragItem *> *)_webView:(WKWebView *)webView willPerformDropWithSession:(id <UIDropSession>)session
516 {
517     return self.overridePerformDropBlock ? self.overridePerformDropBlock(session) : session.items;
518 }
519
520 #pragma mark - _WKInputDelegate
521
522 - (BOOL)_webView:(WKWebView *)webView focusShouldStartInputSession:(id <_WKFocusedElementInfo>)info
523 {
524     return _allowsFocusToStartInputSession;
525 }
526
527 - (void)_webView:(WKWebView *)webView didStartInputSession:(id <_WKFormInputSession>)inputSession
528 {
529     _isDoneWaitingForInputSession = true;
530 }
531
532 @end
533
534 #endif // ENABLE(DATA_INTERACTION)