a4eefd171e481228bc09fd27435c4ea79bcd1568
[WebKit-https.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
34 #import <UIKit/UIDragInteraction.h>
35 #import <UIKit/UIDragItem.h>
36
37 #if USE(APPLE_INTERNAL_SDK)
38 #import <UIKit/UIDragSession.h>
39 #import <UIKit/UIDragging.h>
40 #else
41
42 @protocol UIDraggingInfo <NSObject>
43 @end
44
45 @interface UIDraggingSession : NSObject <UIDraggingInfo>
46 @end
47
48 #endif
49
50 #import <WebKit/WKWebViewPrivate.h>
51 #import <WebKit/_WKFocusedElementInfo.h>
52 #import <WebKit/_WKFormInputSession.h>
53 #import <wtf/RetainPtr.h>
54 #import <wtf/SoftLinking.h>
55
56 SOFT_LINK_FRAMEWORK(UIKit)
57 SOFT_LINK(UIKit, UIApplicationInstantiateSingleton, void, (Class singletonClass), (singletonClass))
58
59 using namespace TestWebKitAPI;
60
61 @interface MockDragDropSession : NSObject <UIDragDropSession> {
62 @private
63     RetainPtr<NSArray> _mockItems;
64     RetainPtr<UIWindow> _window;
65 }
66 @property (nonatomic) CGPoint mockLocationInWindow;
67 @property (nonatomic) BOOL allowMove;
68 @end
69
70 @implementation MockDragDropSession
71
72 - (instancetype)initWithItems:(NSArray <UIDragItem *>*)items location:(CGPoint)locationInWindow window:(UIWindow *)window allowMove:(BOOL)allowMove
73 {
74     if (self = [super init]) {
75         _mockItems = items;
76         _mockLocationInWindow = locationInWindow;
77         _window = window;
78         _allowMove = allowMove;
79     }
80     return self;
81 }
82
83 - (BOOL)allowsMoveOperation
84 {
85     return _allowMove;
86 }
87
88 - (BOOL)isRestrictedToDraggingApplication
89 {
90     return NO;
91 }
92
93 - (BOOL)hasItemsConformingToTypeIdentifiers:(NSArray<NSString *> *)typeIdentifiers
94 {
95     for (NSString *typeIdentifier in typeIdentifiers) {
96         BOOL hasItemConformingToType = NO;
97         for (UIDragItem *item in self.items)
98             hasItemConformingToType |= [[item.itemProvider registeredTypeIdentifiers] containsObject:typeIdentifier];
99         if (!hasItemConformingToType)
100             return NO;
101     }
102     return YES;
103 }
104
105 - (BOOL)canLoadObjectsOfClass:(Class<UIItemProviderReading>)aClass
106 {
107     for (UIDragItem *item in self.items) {
108         if ([item.itemProvider canLoadObjectOfClass:aClass])
109             return YES;
110     }
111     return NO;
112 }
113
114 - (BOOL)canLoadObjectsOfClasses:(NSArray<Class<UIItemProviderReading>> *)classes
115 {
116     for (Class<UIItemProviderReading> aClass in classes) {
117         BOOL canLoad = NO;
118         for (UIDragItem *item in self.items)
119             canLoad |= [item.itemProvider canLoadObjectOfClass:aClass];
120         if (!canLoad)
121             return NO;
122     }
123     return YES;
124 }
125
126 - (NSArray<UIDragItem *> *)items
127 {
128     return _mockItems.get();
129 }
130
131 - (void)setItems:(NSArray<UIDragItem *> *)items
132 {
133     _mockItems = items;
134 }
135
136 - (void)addItems:(NSArray<UIDragItem *> *)items
137 {
138     if (![items count])
139         return;
140
141     if (![_mockItems count])
142         _mockItems = items;
143     else
144         _mockItems = [_mockItems arrayByAddingObjectsFromArray:items];
145 }
146
147 - (CGPoint)locationInView:(UIView *)view
148 {
149     return [_window convertPoint:_mockLocationInWindow toView:view];
150 }
151
152 @end
153
154 NSString * const DataInteractionEnterEventName = @"dragenter";
155 NSString * const DataInteractionOverEventName = @"dragover";
156 NSString * const DataInteractionPerformOperationEventName = @"drop";
157 NSString * const DataInteractionLeaveEventName = @"dragleave";
158 NSString * const DataInteractionStartEventName = @"dragstart";
159
160 @interface MockDataOperationSession : MockDragDropSession <UIDropSession>
161 @property (nonatomic, strong) id localContext;
162 @end
163
164 @implementation MockDataOperationSession
165
166 - (instancetype)initWithProviders:(NSArray<UIItemProvider *> *)providers location:(CGPoint)locationInWindow window:(UIWindow *)window allowMove:(BOOL)allowMove
167 {
168     auto items = adoptNS([[NSMutableArray alloc] init]);
169     for (UIItemProvider *itemProvider in providers)
170         [items addObject:[[[UIDragItem alloc] initWithItemProvider:itemProvider] autorelease]];
171
172     return [super initWithItems:items.get() location:locationInWindow window:window allowMove:allowMove];
173 }
174
175 - (UIDraggingSession *)session
176 {
177     return nil;
178 }
179
180 - (BOOL)isLocal
181 {
182     return YES;
183 }
184
185 - (NSProgress *)progress
186 {
187     return [NSProgress discreteProgressWithTotalUnitCount:100];
188 }
189
190 - (void)setProgressIndicatorStyle:(UIDropSessionProgressIndicatorStyle)progressIndicatorStyle
191 {
192 }
193
194 - (UIDropSessionProgressIndicatorStyle)progressIndicatorStyle
195 {
196     return UIDropSessionProgressIndicatorStyleNone;
197 }
198
199 - (NSUInteger)operationMask
200 {
201     return 0;
202 }
203
204 - (id <UIDragSession>)localDragSession
205 {
206     return nil;
207 }
208
209 - (BOOL)hasItemsConformingToTypeIdentifier:(NSString *)typeIdentifier
210 {
211     ASSERT_NOT_REACHED();
212     return NO;
213 }
214
215 - (BOOL)canCreateItemsOfClass:(Class<UIItemProviderReading>)aClass
216 {
217     ASSERT_NOT_REACHED();
218     return NO;
219 }
220
221 - (NSProgress *)loadObjectsOfClass:(Class<NSItemProviderReading>)aClass completion:(void(^)(NSArray<__kindof id <NSItemProviderReading>> *objects))completion
222 {
223     ASSERT_NOT_REACHED();
224     return nil;
225 }
226
227 @end
228
229 @interface MockDataInteractionSession : MockDragDropSession <UIDragSession>
230 @property (nonatomic, strong) id localContext;
231 @property (nonatomic, strong) id context;
232 @end
233
234 @implementation MockDataInteractionSession
235
236 - (instancetype)initWithWindow:(UIWindow *)window allowMove:(BOOL)allowMove
237 {
238     return [super initWithItems:@[ ] location:CGPointZero window:window allowMove:allowMove];
239 }
240
241 - (NSUInteger)localOperationMask
242 {
243     ASSERT_NOT_REACHED();
244     return 0;
245 }
246
247 - (NSUInteger)externalOperationMask
248 {
249     ASSERT_NOT_REACHED();
250     return 0;
251 }
252
253 - (id)session
254 {
255     return nil;
256 }
257
258 @end
259
260 static double progressIncrementStep = 0.033;
261 static double progressTimeStep = 0.016;
262 static NSString *TestWebKitAPISimulateCancelAllTouchesNotificationName = @"TestWebKitAPISimulateCancelAllTouchesNotificationName";
263
264 static NSArray *dataInteractionEventNames()
265 {
266     static NSArray *eventNames = nil;
267     static dispatch_once_t onceToken;
268     dispatch_once(&onceToken, ^() {
269         eventNames = @[ DataInteractionEnterEventName, DataInteractionOverEventName, DataInteractionPerformOperationEventName, DataInteractionLeaveEventName, DataInteractionStartEventName ];
270     });
271     return eventNames;
272 }
273
274 @interface DataInteractionSimulatorApplication : UIApplication
275 @end
276
277 @implementation DataInteractionSimulatorApplication
278 - (void)_cancelAllTouches
279 {
280     [[NSNotificationCenter defaultCenter] postNotificationName:TestWebKitAPISimulateCancelAllTouchesNotificationName object:nil];
281 }
282 @end
283
284 @implementation DataInteractionSimulator
285
286 - (instancetype)initWithWebView:(TestWKWebView *)webView
287 {
288     if (self = [super init]) {
289         _webView = webView;
290         _shouldEnsureUIApplication = NO;
291         _shouldAllowMoveOperation = YES;
292         _isDoneWaitingForInputSession = true;
293         [_webView setUIDelegate:self];
294         [_webView _setInputDelegate:self];
295     }
296     return self;
297 }
298
299 - (void)dealloc
300 {
301     if ([_webView UIDelegate] == self)
302         [_webView setUIDelegate:nil];
303
304     if ([_webView _inputDelegate] == self)
305         [_webView _setInputDelegate:nil];
306
307     [super dealloc];
308 }
309
310 - (void)_resetSimulatedState
311 {
312     _phase = DataInteractionBeginning;
313     _currentProgress = 0;
314     _isDoneWithCurrentRun = false;
315     _observedEventNames = adoptNS([[NSMutableArray alloc] init]);
316     _finalSelectionRects = @[ ];
317     _dataInteractionSession = nil;
318     _dataOperationSession = nil;
319     _shouldPerformOperation = NO;
320     _lastKnownDragCaretRect = CGRectZero;
321     _remainingAdditionalItemRequestLocationsByProgress = nil;
322     _queuedAdditionalItemRequestLocations = adoptNS([[NSMutableArray alloc] init]);
323 }
324
325 - (NSArray *)observedEventNames
326 {
327     return _observedEventNames.get();
328 }
329
330 - (void)simulateAllTouchesCanceled:(NSNotification *)notification
331 {
332     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_advanceProgress) object:nil];
333     _phase = DataInteractionCancelled;
334     _currentProgress = 1;
335     _isDoneWithCurrentRun = true;
336     if (_dataInteractionSession)
337         [_webView _simulateDataInteractionSessionDidEnd:_dataInteractionSession.get()];
338 }
339
340 - (void)runFrom:(CGPoint)startLocation to:(CGPoint)endLocation
341 {
342     [self runFrom:startLocation to:endLocation additionalItemRequestLocations:nil];
343 }
344
345 - (void)runFrom:(CGPoint)startLocation to:(CGPoint)endLocation additionalItemRequestLocations:(ProgressToCGPointValueMap)additionalItemRequestLocations
346 {
347     NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
348     [defaultCenter addObserver:self selector:@selector(simulateAllTouchesCanceled:) name:TestWebKitAPISimulateCancelAllTouchesNotificationName object:nil];
349
350     if (_shouldEnsureUIApplication)
351         UIApplicationInstantiateSingleton([DataInteractionSimulatorApplication class]);
352
353     [self _resetSimulatedState];
354
355     if (additionalItemRequestLocations)
356         _remainingAdditionalItemRequestLocationsByProgress = adoptNS([additionalItemRequestLocations mutableCopy]);
357
358     RetainPtr<DataInteractionSimulator> strongSelf = self;
359     for (NSString *eventName in dataInteractionEventNames()) {
360         DataInteractionSimulator *weakSelf = strongSelf.get();
361         [weakSelf->_webView performAfterReceivingMessage:eventName action:^() {
362             [weakSelf->_observedEventNames addObject:eventName];
363         }];
364     }
365
366     _startLocation = startLocation;
367     _endLocation = endLocation;
368
369     if (self.externalItemProviders.count) {
370         _dataOperationSession = adoptNS([[MockDataOperationSession alloc] initWithProviders:self.externalItemProviders location:_startLocation window:[_webView window] allowMove:self.shouldAllowMoveOperation]);
371         _phase = DataInteractionBegan;
372         [self _advanceProgress];
373     } else {
374         _dataInteractionSession = adoptNS([[MockDataInteractionSession alloc] initWithWindow:[_webView window] allowMove:self.shouldAllowMoveOperation]);
375         [_dataInteractionSession setMockLocationInWindow:_startLocation];
376         [_webView _simulatePrepareForDataInteractionSession:_dataInteractionSession.get() completion:^() {
377             DataInteractionSimulator *weakSelf = strongSelf.get();
378             if (weakSelf->_phase == DataInteractionCancelled)
379                 return;
380
381             weakSelf->_phase = DataInteractionBeginning;
382             [weakSelf _advanceProgress];
383         }];
384     }
385
386     Util::run(&_isDoneWithCurrentRun);
387     [_webView clearMessageHandlers:dataInteractionEventNames()];
388     _finalSelectionRects = [_webView selectionRectsAfterPresentationUpdate];
389
390     [defaultCenter removeObserver:self];
391 }
392
393 - (NSArray *)finalSelectionRects
394 {
395     return _finalSelectionRects.get();
396 }
397
398 - (void)_concludeDataInteractionAndPerformOperationIfNecessary
399 {
400     _lastKnownDragCaretRect = [_webView _dragCaretRect];
401     if (_shouldPerformOperation) {
402         [_webView _simulateDataInteractionPerformOperation:_dataOperationSession.get()];
403         _phase = DataInteractionPerforming;
404     } else {
405         _isDoneWithCurrentRun = YES;
406         _phase = DataInteractionCancelled;
407     }
408
409     [_webView _simulateDataInteractionEnded:_dataOperationSession.get()];
410
411     if (_dataInteractionSession)
412         [_webView _simulateDataInteractionSessionDidEnd:_dataInteractionSession.get()];
413 }
414
415 - (void)_enqueuePendingAdditionalItemRequestLocations
416 {
417     NSMutableArray *progressValuesToRemove = [NSMutableArray array];
418     for (NSNumber *progressValue in _remainingAdditionalItemRequestLocationsByProgress.get()) {
419         double progress = progressValue.doubleValue;
420         if (progress > _currentProgress)
421             continue;
422         [progressValuesToRemove addObject:progressValue];
423         [_queuedAdditionalItemRequestLocations addObject:[_remainingAdditionalItemRequestLocationsByProgress objectForKey:progressValue]];
424     }
425
426     for (NSNumber *progressToRemove in progressValuesToRemove)
427         [_remainingAdditionalItemRequestLocationsByProgress removeObjectForKey:progressToRemove];
428 }
429
430 - (BOOL)_sendQueuedAdditionalItemRequest
431 {
432     if (![_queuedAdditionalItemRequestLocations count])
433         return NO;
434
435     RetainPtr<NSValue> requestLocationValue = [_queuedAdditionalItemRequestLocations objectAtIndex:0];
436     [_queuedAdditionalItemRequestLocations removeObjectAtIndex:0];
437
438     auto requestLocation = [[_webView window] convertPoint:[requestLocationValue CGPointValue] toView:_webView.get()];
439     [_webView _simulateItemsForAddingToSession:_dataInteractionSession.get() atLocation:requestLocation completion:[dragSession = _dataInteractionSession, dropSession = _dataOperationSession] (NSArray *items) {
440         [dragSession addItems:items];
441         [dropSession addItems:items];
442     }];
443     return YES;
444 }
445
446 - (void)_advanceProgress
447 {
448     [self _enqueuePendingAdditionalItemRequestLocations];
449     if ([self _sendQueuedAdditionalItemRequest]) {
450         [self _scheduleAdvanceProgress];
451         return;
452     }
453
454     _lastKnownDragCaretRect = [_webView _dragCaretRect];
455     _currentProgress += progressIncrementStep;
456     CGPoint locationInWindow = self._currentLocation;
457     [_dataInteractionSession setMockLocationInWindow:locationInWindow];
458     [_dataOperationSession setMockLocationInWindow:locationInWindow];
459
460     if (_currentProgress >= 1) {
461         _currentProgress = 1;
462         [self _concludeDataInteractionAndPerformOperationIfNecessary];
463         return;
464     }
465
466     switch (_phase) {
467     case DataInteractionBeginning: {
468         NSMutableArray<UIItemProvider *> *itemProviders = [NSMutableArray array];
469         NSArray *items = [_webView _simulatedItemsForSession:_dataInteractionSession.get()];
470         if (!items.count) {
471             _phase = DataInteractionCancelled;
472             _currentProgress = 1;
473             _isDoneWithCurrentRun = true;
474             return;
475         }
476
477         for (UIDragItem *item in items)
478             [itemProviders addObject:item.itemProvider];
479
480         _dataOperationSession = adoptNS([[MockDataOperationSession alloc] initWithProviders:itemProviders location:self._currentLocation window:[_webView window] allowMove:self.shouldAllowMoveOperation]);
481         [_dataInteractionSession setItems:items];
482         _sourceItemProviders = itemProviders;
483         if (self.showCustomActionSheetBlock) {
484             // Defer progress until the custom action sheet is dismissed.
485             auto startLocationInView = [[_webView window] convertPoint:_startLocation toView:_webView.get()];
486             [_webView _simulateLongPressActionAtLocation:startLocationInView];
487             return;
488         }
489
490         [_webView _simulateWillBeginDataInteractionWithSession:_dataInteractionSession.get()];
491
492         RetainPtr<WKWebView> retainedWebView = _webView;
493         dispatch_async(dispatch_get_main_queue(), ^() {
494             [retainedWebView resignFirstResponder];
495         });
496
497         _phase = DataInteractionBegan;
498         break;
499     }
500     case DataInteractionBegan:
501         [_webView _simulateDataInteractionEntered:_dataOperationSession.get()];
502         _phase = DataInteractionEntered;
503         break;
504     case DataInteractionEntered: {
505         auto operation = static_cast<UIDropOperation>([_webView _simulateDataInteractionUpdated:_dataOperationSession.get()]);
506         _shouldPerformOperation = operation == UIDropOperationCopy || ([_dataOperationSession allowsMoveOperation] && operation != UIDropOperationCancel);
507         break;
508     }
509     default:
510         break;
511     }
512
513     [self _scheduleAdvanceProgress];
514 }
515
516 - (CGPoint)_currentLocation
517 {
518     CGFloat distanceX = _endLocation.x - _startLocation.x;
519     CGFloat distanceY = _endLocation.y - _startLocation.y;
520     return CGPointMake(_startLocation.x + _currentProgress * distanceX, _startLocation.y + _currentProgress * distanceY);
521 }
522
523 - (void)_scheduleAdvanceProgress
524 {
525     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_advanceProgress) object:nil];
526     [self performSelector:@selector(_advanceProgress) withObject:nil afterDelay:progressTimeStep];
527 }
528
529 - (NSArray *)sourceItemProviders
530 {
531     return _sourceItemProviders.get();
532 }
533
534 - (NSArray *)externalItemProviders
535 {
536     return _externalItemProviders.get();
537 }
538
539 - (void)setExternalItemProviders:(NSArray *)externalItemProviders
540 {
541     _externalItemProviders = adoptNS([externalItemProviders copy]);
542 }
543
544 - (DataInteractionPhase)phase
545 {
546     return _phase;
547 }
548
549 - (CGRect)lastKnownDragCaretRect
550 {
551     return _lastKnownDragCaretRect;
552 }
553
554 - (void)waitForInputSession
555 {
556     _isDoneWaitingForInputSession = false;
557
558     // Waiting for an input session implies that we should allow input sessions to begin.
559     self.allowsFocusToStartInputSession = YES;
560
561     Util::run(&_isDoneWaitingForInputSession);
562 }
563
564 #pragma mark - WKUIDelegatePrivate
565
566 - (void)_webView:(WKWebView *)webView dataInteractionOperationWasHandled:(BOOL)handled forSession:(id)session itemProviders:(NSArray<UIItemProvider *> *)itemProviders
567 {
568     _isDoneWithCurrentRun = true;
569
570     if (self.dataInteractionOperationCompletionBlock)
571         self.dataInteractionOperationCompletionBlock(handled, itemProviders);
572 }
573
574 - (NSUInteger)_webView:(WKWebView *)webView willUpdateDataInteractionOperationToOperation:(NSUInteger)operation forSession:(id)session
575 {
576     return self.overrideDataInteractionOperationBlock ? self.overrideDataInteractionOperationBlock(operation, session) : operation;
577 }
578
579 - (NSArray *)_webView:(WKWebView *)webView adjustedDataInteractionItemProvidersForItemProvider:(UIItemProvider *)itemProvider representingObjects:(NSArray *)representingObjects additionalData:(NSDictionary *)additionalData
580 {
581     return self.convertItemProvidersBlock ? self.convertItemProvidersBlock(itemProvider, representingObjects, additionalData) : @[ itemProvider ];
582 }
583
584 - (BOOL)_webView:(WKWebView *)webView showCustomSheetForElement:(_WKActivatedElementInfo *)element
585 {
586     if (!self.showCustomActionSheetBlock)
587         return NO;
588
589     RetainPtr<DataInteractionSimulator> strongSelf = self;
590     dispatch_async(dispatch_get_main_queue(), ^() {
591         DataInteractionSimulator *weakSelf = strongSelf.get();
592         [weakSelf->_webView _simulateWillBeginDataInteractionWithSession:weakSelf->_dataInteractionSession.get()];
593         weakSelf->_phase = DataInteractionBegan;
594         [weakSelf _scheduleAdvanceProgress];
595     });
596
597     return self.showCustomActionSheetBlock(element);
598 }
599
600 - (NSArray<UIDragItem *> *)_webView:(WKWebView *)webView willPerformDropWithSession:(id <UIDropSession>)session
601 {
602     return self.overridePerformDropBlock ? self.overridePerformDropBlock(session) : session.items;
603 }
604
605 #pragma mark - _WKInputDelegate
606
607 - (BOOL)_webView:(WKWebView *)webView focusShouldStartInputSession:(id <_WKFocusedElementInfo>)info
608 {
609     return _allowsFocusToStartInputSession;
610 }
611
612 - (void)_webView:(WKWebView *)webView didStartInputSession:(id <_WKFormInputSession>)inputSession
613 {
614     _isDoneWaitingForInputSession = true;
615 }
616
617 @end
618
619 #endif // ENABLE(DATA_INTERACTION)