cc4af793c5478501c725587abdb317b235a98bf4
[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 - (CGPoint)locationInView:(UIView *)view
137 {
138     return [_window convertPoint:_mockLocationInWindow toView:view];
139 }
140
141 @end
142
143 NSString * const DataInteractionEnterEventName = @"dragenter";
144 NSString * const DataInteractionOverEventName = @"dragover";
145 NSString * const DataInteractionPerformOperationEventName = @"drop";
146 NSString * const DataInteractionLeaveEventName = @"dragleave";
147 NSString * const DataInteractionStartEventName = @"dragstart";
148
149 @interface MockDataOperationSession : MockDragDropSession <UIDropSession>
150 @property (nonatomic, strong) id localContext;
151 @end
152
153 @implementation MockDataOperationSession
154
155 - (instancetype)initWithProviders:(NSArray<UIItemProvider *> *)providers location:(CGPoint)locationInWindow window:(UIWindow *)window allowMove:(BOOL)allowMove
156 {
157     auto items = adoptNS([[NSMutableArray alloc] init]);
158     for (UIItemProvider *itemProvider in providers)
159         [items addObject:[[[UIDragItem alloc] initWithItemProvider:itemProvider] autorelease]];
160
161     return [super initWithItems:items.get() location:locationInWindow window:window allowMove:allowMove];
162 }
163
164 - (UIDraggingSession *)session
165 {
166     return nil;
167 }
168
169 - (BOOL)isLocal
170 {
171     return YES;
172 }
173
174 - (NSProgress *)progress
175 {
176     return [NSProgress discreteProgressWithTotalUnitCount:100];
177 }
178
179 - (void)setProgressIndicatorStyle:(UIDropSessionProgressIndicatorStyle)progressIndicatorStyle
180 {
181 }
182
183 - (UIDropSessionProgressIndicatorStyle)progressIndicatorStyle
184 {
185     return UIDropSessionProgressIndicatorStyleNone;
186 }
187
188 - (NSUInteger)operationMask
189 {
190     return 0;
191 }
192
193 - (id <UIDragSession>)localDragSession
194 {
195     return nil;
196 }
197
198 - (BOOL)hasItemsConformingToTypeIdentifier:(NSString *)typeIdentifier
199 {
200     ASSERT_NOT_REACHED();
201     return NO;
202 }
203
204 - (BOOL)canCreateItemsOfClass:(Class<UIItemProviderReading>)aClass
205 {
206     ASSERT_NOT_REACHED();
207     return NO;
208 }
209
210 - (NSProgress *)loadObjectsOfClass:(Class<NSItemProviderReading>)aClass completion:(void(^)(NSArray<__kindof id <NSItemProviderReading>> *objects))completion
211 {
212     ASSERT_NOT_REACHED();
213     return nil;
214 }
215
216 @end
217
218 @interface MockDataInteractionSession : MockDragDropSession <UIDragSession>
219 @property (nonatomic, strong) id localContext;
220 @property (nonatomic, strong) id context;
221 @end
222
223 @implementation MockDataInteractionSession
224
225 - (instancetype)initWithWindow:(UIWindow *)window allowMove:(BOOL)allowMove
226 {
227     return [super initWithItems:@[ ] location:CGPointZero window:window allowMove:allowMove];
228 }
229
230 - (NSUInteger)localOperationMask
231 {
232     ASSERT_NOT_REACHED();
233     return 0;
234 }
235
236 - (NSUInteger)externalOperationMask
237 {
238     ASSERT_NOT_REACHED();
239     return 0;
240 }
241
242 - (id)session
243 {
244     return nil;
245 }
246
247 @end
248
249 static double progressIncrementStep = 0.033;
250 static double progressTimeStep = 0.016;
251 static NSString *TestWebKitAPISimulateCancelAllTouchesNotificationName = @"TestWebKitAPISimulateCancelAllTouchesNotificationName";
252
253 static NSArray *dataInteractionEventNames()
254 {
255     static NSArray *eventNames = nil;
256     static dispatch_once_t onceToken;
257     dispatch_once(&onceToken, ^() {
258         eventNames = @[ DataInteractionEnterEventName, DataInteractionOverEventName, DataInteractionPerformOperationEventName, DataInteractionLeaveEventName, DataInteractionStartEventName ];
259     });
260     return eventNames;
261 }
262
263 @interface DataInteractionSimulatorApplication : UIApplication
264 @end
265
266 @implementation DataInteractionSimulatorApplication
267 - (void)_cancelAllTouches
268 {
269     [[NSNotificationCenter defaultCenter] postNotificationName:TestWebKitAPISimulateCancelAllTouchesNotificationName object:nil];
270 }
271 @end
272
273 @implementation DataInteractionSimulator
274
275 - (instancetype)initWithWebView:(TestWKWebView *)webView
276 {
277     if (self = [super init]) {
278         _webView = webView;
279         _shouldEnsureUIApplication = NO;
280         _shouldAllowMoveOperation = YES;
281         _isDoneWaitingForInputSession = true;
282         [_webView setUIDelegate:self];
283         [_webView _setInputDelegate:self];
284     }
285     return self;
286 }
287
288 - (void)dealloc
289 {
290     if ([_webView UIDelegate] == self)
291         [_webView setUIDelegate:nil];
292
293     if ([_webView _inputDelegate] == self)
294         [_webView _setInputDelegate:nil];
295
296     [super dealloc];
297 }
298
299 - (void)_resetSimulatedState
300 {
301     _phase = DataInteractionBeginning;
302     _currentProgress = 0;
303     _isDoneWithCurrentRun = false;
304     _observedEventNames = adoptNS([[NSMutableArray alloc] init]);
305     _finalSelectionRects = @[ ];
306     _dataInteractionSession = nil;
307     _dataOperationSession = nil;
308     _shouldPerformOperation = NO;
309     _lastKnownDragCaretRect = CGRectZero;
310 }
311
312 - (NSArray *)observedEventNames
313 {
314     return _observedEventNames.get();
315 }
316
317 - (void)simulateAllTouchesCanceled:(NSNotification *)notification
318 {
319     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_advanceProgress) object:nil];
320     _phase = DataInteractionCancelled;
321     _currentProgress = 1;
322     _isDoneWithCurrentRun = true;
323     if (_dataInteractionSession)
324         [_webView _simulateDataInteractionSessionDidEnd:_dataInteractionSession.get()];
325 }
326
327 - (void)runFrom:(CGPoint)startLocation to:(CGPoint)endLocation
328 {
329     NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
330     [defaultCenter addObserver:self selector:@selector(simulateAllTouchesCanceled:) name:TestWebKitAPISimulateCancelAllTouchesNotificationName object:nil];
331
332     if (_shouldEnsureUIApplication)
333         UIApplicationInstantiateSingleton([DataInteractionSimulatorApplication class]);
334
335     [self _resetSimulatedState];
336
337     RetainPtr<DataInteractionSimulator> strongSelf = self;
338     for (NSString *eventName in dataInteractionEventNames()) {
339         DataInteractionSimulator *weakSelf = strongSelf.get();
340         [weakSelf->_webView performAfterReceivingMessage:eventName action:^() {
341             [weakSelf->_observedEventNames addObject:eventName];
342         }];
343     }
344
345     _startLocation = startLocation;
346     _endLocation = endLocation;
347
348     if (self.externalItemProviders.count) {
349         _dataOperationSession = adoptNS([[MockDataOperationSession alloc] initWithProviders:self.externalItemProviders location:_startLocation window:[_webView window] allowMove:self.shouldAllowMoveOperation]);
350         _phase = DataInteractionBegan;
351         [self _advanceProgress];
352     } else {
353         _dataInteractionSession = adoptNS([[MockDataInteractionSession alloc] initWithWindow:[_webView window] allowMove:self.shouldAllowMoveOperation]);
354         [_dataInteractionSession setMockLocationInWindow:_startLocation];
355         [_webView _simulatePrepareForDataInteractionSession:_dataInteractionSession.get() completion:^() {
356             DataInteractionSimulator *weakSelf = strongSelf.get();
357             if (weakSelf->_phase == DataInteractionCancelled)
358                 return;
359
360             weakSelf->_phase = DataInteractionBeginning;
361             [weakSelf _advanceProgress];
362         }];
363     }
364
365     Util::run(&_isDoneWithCurrentRun);
366     [_webView clearMessageHandlers:dataInteractionEventNames()];
367     _finalSelectionRects = [_webView selectionRectsAfterPresentationUpdate];
368
369     [defaultCenter removeObserver:self];
370 }
371
372 - (NSArray *)finalSelectionRects
373 {
374     return _finalSelectionRects.get();
375 }
376
377 - (void)_concludeDataInteractionAndPerformOperationIfNecessary
378 {
379     _lastKnownDragCaretRect = [_webView _dragCaretRect];
380     if (_shouldPerformOperation) {
381         [_webView _simulateDataInteractionPerformOperation:_dataOperationSession.get()];
382         _phase = DataInteractionPerforming;
383     } else {
384         _isDoneWithCurrentRun = YES;
385         _phase = DataInteractionCancelled;
386     }
387
388     [_webView _simulateDataInteractionEnded:_dataOperationSession.get()];
389
390     if (_dataInteractionSession)
391         [_webView _simulateDataInteractionSessionDidEnd:_dataInteractionSession.get()];
392 }
393
394 - (void)_advanceProgress
395 {
396     _lastKnownDragCaretRect = [_webView _dragCaretRect];
397     _currentProgress += progressIncrementStep;
398     CGPoint locationInWindow = self._currentLocation;
399     [_dataInteractionSession setMockLocationInWindow:locationInWindow];
400     [_dataOperationSession setMockLocationInWindow:locationInWindow];
401
402     if (_currentProgress >= 1) {
403         _currentProgress = 1;
404         [self _concludeDataInteractionAndPerformOperationIfNecessary];
405         return;
406     }
407
408     switch (_phase) {
409     case DataInteractionBeginning: {
410         NSMutableArray<UIItemProvider *> *itemProviders = [NSMutableArray array];
411         NSArray *items = [_webView _simulatedItemsForSession:_dataInteractionSession.get()];
412         if (!items.count) {
413             _phase = DataInteractionCancelled;
414             _currentProgress = 1;
415             _isDoneWithCurrentRun = true;
416             return;
417         }
418
419         for (UIDragItem *item in items)
420             [itemProviders addObject:item.itemProvider];
421
422         _dataOperationSession = adoptNS([[MockDataOperationSession alloc] initWithProviders:itemProviders location:self._currentLocation window:[_webView window] allowMove:self.shouldAllowMoveOperation]);
423         [_dataInteractionSession setItems:items];
424         _sourceItemProviders = itemProviders;
425         if (self.showCustomActionSheetBlock) {
426             // Defer progress until the custom action sheet is dismissed.
427             auto startLocationInView = [[_webView window] convertPoint:_startLocation toView:_webView.get()];
428             [_webView _simulateLongPressActionAtLocation:startLocationInView];
429             return;
430         }
431
432         [_webView _simulateWillBeginDataInteractionWithSession:_dataInteractionSession.get()];
433
434         RetainPtr<WKWebView> retainedWebView = _webView;
435         dispatch_async(dispatch_get_main_queue(), ^() {
436             [retainedWebView resignFirstResponder];
437         });
438
439         _phase = DataInteractionBegan;
440         break;
441     }
442     case DataInteractionBegan:
443         [_webView _simulateDataInteractionEntered:_dataOperationSession.get()];
444         _phase = DataInteractionEntered;
445         break;
446     case DataInteractionEntered: {
447         auto operation = static_cast<UIDropOperation>([_webView _simulateDataInteractionUpdated:_dataOperationSession.get()]);
448         _shouldPerformOperation = operation == UIDropOperationCopy || ([_dataOperationSession allowsMoveOperation] && operation != UIDropOperationCancel);
449         break;
450     }
451     default:
452         break;
453     }
454
455     [self _scheduleAdvanceProgress];
456 }
457
458 - (CGPoint)_currentLocation
459 {
460     CGFloat distanceX = _endLocation.x - _startLocation.x;
461     CGFloat distanceY = _endLocation.y - _startLocation.y;
462     return CGPointMake(_startLocation.x + _currentProgress * distanceX, _startLocation.y + _currentProgress * distanceY);
463 }
464
465 - (void)_scheduleAdvanceProgress
466 {
467     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_advanceProgress) object:nil];
468     [self performSelector:@selector(_advanceProgress) withObject:nil afterDelay:progressTimeStep];
469 }
470
471 - (NSArray *)sourceItemProviders
472 {
473     return _sourceItemProviders.get();
474 }
475
476 - (NSArray *)externalItemProviders
477 {
478     return _externalItemProviders.get();
479 }
480
481 - (void)setExternalItemProviders:(NSArray *)externalItemProviders
482 {
483     _externalItemProviders = adoptNS([externalItemProviders copy]);
484 }
485
486 - (DataInteractionPhase)phase
487 {
488     return _phase;
489 }
490
491 - (CGRect)lastKnownDragCaretRect
492 {
493     return _lastKnownDragCaretRect;
494 }
495
496 - (void)waitForInputSession
497 {
498     _isDoneWaitingForInputSession = false;
499
500     // Waiting for an input session implies that we should allow input sessions to begin.
501     self.allowsFocusToStartInputSession = YES;
502
503     Util::run(&_isDoneWaitingForInputSession);
504 }
505
506 #pragma mark - WKUIDelegatePrivate
507
508 - (void)_webView:(WKWebView *)webView dataInteractionOperationWasHandled:(BOOL)handled forSession:(id)session itemProviders:(NSArray<UIItemProvider *> *)itemProviders
509 {
510     _isDoneWithCurrentRun = true;
511
512     if (self.dataInteractionOperationCompletionBlock)
513         self.dataInteractionOperationCompletionBlock(handled, itemProviders);
514 }
515
516 - (NSUInteger)_webView:(WKWebView *)webView willUpdateDataInteractionOperationToOperation:(NSUInteger)operation forSession:(id)session
517 {
518     return self.overrideDataInteractionOperationBlock ? self.overrideDataInteractionOperationBlock(operation, session) : operation;
519 }
520
521 - (NSArray *)_webView:(WKWebView *)webView adjustedDataInteractionItemProvidersForItemProvider:(UIItemProvider *)itemProvider representingObjects:(NSArray *)representingObjects additionalData:(NSDictionary *)additionalData
522 {
523     return self.convertItemProvidersBlock ? self.convertItemProvidersBlock(itemProvider, representingObjects, additionalData) : @[ itemProvider ];
524 }
525
526 - (BOOL)_webView:(WKWebView *)webView showCustomSheetForElement:(_WKActivatedElementInfo *)element
527 {
528     if (!self.showCustomActionSheetBlock)
529         return NO;
530
531     RetainPtr<DataInteractionSimulator> strongSelf = self;
532     dispatch_async(dispatch_get_main_queue(), ^() {
533         DataInteractionSimulator *weakSelf = strongSelf.get();
534         [weakSelf->_webView _simulateWillBeginDataInteractionWithSession:weakSelf->_dataInteractionSession.get()];
535         weakSelf->_phase = DataInteractionBegan;
536         [weakSelf _scheduleAdvanceProgress];
537     });
538
539     return self.showCustomActionSheetBlock(element);
540 }
541
542 - (NSArray<UIDragItem *> *)_webView:(WKWebView *)webView willPerformDropWithSession:(id <UIDropSession>)session
543 {
544     return self.overridePerformDropBlock ? self.overridePerformDropBlock(session) : session.items;
545 }
546
547 #pragma mark - _WKInputDelegate
548
549 - (BOOL)_webView:(WKWebView *)webView focusShouldStartInputSession:(id <_WKFocusedElementInfo>)info
550 {
551     return _allowsFocusToStartInputSession;
552 }
553
554 - (void)_webView:(WKWebView *)webView didStartInputSession:(id <_WKFormInputSession>)inputSession
555 {
556     _isDoneWaitingForInputSession = true;
557 }
558
559 @end
560
561 #endif // ENABLE(DATA_INTERACTION)