OLD | NEW |
(Empty) | |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h" |
| 6 |
| 7 #include "base/ios/block_types.h" |
| 8 #include "base/mac/foundation_util.h" |
| 9 #include "base/mac/scoped_block.h" |
| 10 #include "base/mac/scoped_nsobject.h" |
| 11 #include "base/memory/scoped_ptr.h" |
| 12 #include "base/strings/sys_string_conversions.h" |
| 13 #include "base/strings/utf_string_conversions.h" |
| 14 #import "components/autofill/ios/browser/js_suggestion_manager.h" |
| 15 #import "ios/chrome/browser/autofill/form_input_accessory_view.h" |
| 16 #import "ios/chrome/browser/passwords/password_generation_utils.h" |
| 17 #include "ios/web/public/test/crw_test_js_injection_receiver.h" |
| 18 #include "ios/web/public/url_scheme_util.h" |
| 19 #import "ios/web/public/web_state/crw_web_view_proxy.h" |
| 20 #include "ios/web/public/web_state/url_verification_constants.h" |
| 21 #include "ios/web/public/web_state/web_state.h" |
| 22 #include "url/gurl.h" |
| 23 |
| 24 namespace ios_internal { |
| 25 namespace autofill { |
| 26 NSString* const kFormSuggestionAssistButtonPreviousElement = @"previousTap"; |
| 27 NSString* const kFormSuggestionAssistButtonNextElement = @"nextTap"; |
| 28 NSString* const kFormSuggestionAssistButtonDone = @"done"; |
| 29 } // namespace autofill |
| 30 } // namespace ios_internal |
| 31 |
| 32 namespace { |
| 33 |
| 34 // Finds all views of a particular kind if class |klass| in the subview |
| 35 // hierarchy of the given |root| view. |
| 36 NSArray* FindDescendantsOfClass(UIView* root, Class klass) { |
| 37 DCHECK(root); |
| 38 NSMutableArray* viewsToExamine = [NSMutableArray arrayWithObject:root]; |
| 39 NSMutableArray* descendants = [NSMutableArray array]; |
| 40 |
| 41 while ([viewsToExamine count]) { |
| 42 UIView* view = [viewsToExamine lastObject]; |
| 43 if ([view isKindOfClass:klass]) |
| 44 [descendants addObject:view]; |
| 45 |
| 46 [viewsToExamine removeLastObject]; |
| 47 [viewsToExamine addObjectsFromArray:[view subviews]]; |
| 48 } |
| 49 |
| 50 return descendants; |
| 51 } |
| 52 |
| 53 // Finds all UIToolbarItems associated with a given UIToolbar |toolbar| with |
| 54 // action selectors with a name that containts the action name specified by |
| 55 // |actionName|. |
| 56 NSArray* FindToolbarItemsForActionName(UIToolbar* toolbar, |
| 57 NSString* actionName) { |
| 58 NSMutableArray* toolbarItems = [NSMutableArray array]; |
| 59 |
| 60 for (UIBarButtonItem* item in [toolbar items]) { |
| 61 SEL itemAction = [item action]; |
| 62 if (!itemAction) |
| 63 continue; |
| 64 NSString* itemActionName = NSStringFromSelector(itemAction); |
| 65 |
| 66 // We don't do a strict string match for the action name. |
| 67 if ([itemActionName rangeOfString:actionName].location != NSNotFound) |
| 68 [toolbarItems addObject:item]; |
| 69 } |
| 70 |
| 71 return toolbarItems; |
| 72 } |
| 73 |
| 74 // Finds all UIToolbarItem(s) with action selectors of the name specified by |
| 75 // |actionName| in any UIToolbars in the view hierarchy below |root|. |
| 76 NSArray* FindDescendantToolbarItemsForActionName(UIView* root, |
| 77 NSString* actionName) { |
| 78 NSMutableArray* descendants = [NSMutableArray array]; |
| 79 |
| 80 NSArray* toolbars = FindDescendantsOfClass(root, [UIToolbar class]); |
| 81 for (UIToolbar* toolbar in toolbars) { |
| 82 [descendants |
| 83 addObjectsFromArray:FindToolbarItemsForActionName(toolbar, actionName)]; |
| 84 } |
| 85 |
| 86 return descendants; |
| 87 } |
| 88 |
| 89 // Computes the frame of each part of the accessory view of the keyboard. It is |
| 90 // assumed that the keyboard has either two parts (when it is split) or one part |
| 91 // (when it is merged). |
| 92 // |
| 93 // If there are two parts, the frame of the left part is returned in |
| 94 // |leftFrame| and the frame of the right part is returned in |rightFrame|. |
| 95 // If there is only one part, the frame is returned in |leftFrame| and |
| 96 // |rightFrame| has size zero. |
| 97 // |
| 98 // Heuristics are used to compute this information. It returns true if the |
| 99 // number of |inputAccessoryView.subviews| is not 2. |
| 100 bool ComputeFramesOfKeyboardParts(UIView* inputAccessoryView, |
| 101 CGRect* leftFrame, |
| 102 CGRect* rightFrame) { |
| 103 // It is observed (on iOS 6) there are always two subviews in the original |
| 104 // input accessory view. When the keyboard is split, each subview represents |
| 105 // one part of the accesssary view of the keyboard. When the keyboard is |
| 106 // merged, one subview has the same frame as that of the whole accessory view |
| 107 // and the other has zero size with the screen width as origin.x. |
| 108 // The computation here is based on this observation. |
| 109 NSArray* subviews = inputAccessoryView.subviews; |
| 110 if (subviews.count != 2) |
| 111 return false; |
| 112 |
| 113 CGRect first_frame = static_cast<UIView*>(subviews[0]).frame; |
| 114 CGRect second_frame = static_cast<UIView*>(subviews[1]).frame; |
| 115 if (CGRectGetMinX(first_frame) < CGRectGetMinX(second_frame) || |
| 116 CGRectGetWidth(second_frame) == 0) { |
| 117 *leftFrame = first_frame; |
| 118 *rightFrame = second_frame; |
| 119 } else { |
| 120 *rightFrame = first_frame; |
| 121 *leftFrame = second_frame; |
| 122 } |
| 123 return true; |
| 124 } |
| 125 |
| 126 } // namespace |
| 127 |
| 128 @interface FormInputAccessoryViewController () |
| 129 |
| 130 // Allows injection of the JsSuggestionManager. |
| 131 - (instancetype)initWithWebState:(web::WebState*)webState |
| 132 JSSuggestionManager:(JsSuggestionManager*)JSSuggestionManager |
| 133 providers:(NSArray*)providers; |
| 134 |
| 135 // Called when the keyboard did change frame. |
| 136 - (void)keyboardDidChangeFrame:(NSNotification*)notification; |
| 137 |
| 138 // Called when the keyboard is dismissed. |
| 139 - (void)keyboardDidHide:(NSNotification*)notification; |
| 140 |
| 141 // Hides the subviews in |accessoryView|. |
| 142 - (void)hideSubviewsInOriginalAccessoryView:(UIView*)accessoryView; |
| 143 |
| 144 // Attempts to execute/tap/send-an-event-to the iOS built-in "next" and |
| 145 // "previous" form assist controls. Returns NO if this attempt failed, YES |
| 146 // otherwise. [HACK] |
| 147 - (BOOL)executeFormAssistAction:(NSString*)actionName; |
| 148 |
| 149 // Runs |block| while allowing the keyboard to be displayed as a result of focus |
| 150 // changes caused by |block|. |
| 151 - (void)runBlockAllowingKeyboardDisplay:(ProceduralBlock)block; |
| 152 |
| 153 // Asynchronously retrieves an accessory view from |_providers|. |
| 154 - (void)retrieveAccessoryViewForForm:(const std::string&)formName |
| 155 field:(const std::string&)fieldName |
| 156 value:(const std::string&)value |
| 157 type:(const std::string&)type |
| 158 webState:(web::WebState*)webState; |
| 159 |
| 160 // Clears the current custom accessory view and restores the default. |
| 161 - (void)reset; |
| 162 |
| 163 // The current web state. |
| 164 @property(nonatomic, readonly) web::WebState* webState; |
| 165 |
| 166 // The current web view proxy. |
| 167 @property(nonatomic, readonly) id<CRWWebViewProxy> webViewProxy; |
| 168 |
| 169 @end |
| 170 |
| 171 @implementation FormInputAccessoryViewController { |
| 172 // Bridge to observe the web state from Objective-C. |
| 173 scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge; |
| 174 |
| 175 // Last registered keyboard rectangle. |
| 176 CGRect _keyboardFrame; |
| 177 |
| 178 // The custom view that should be shown in the input accessory view. |
| 179 base::scoped_nsobject<UIView> _customAccessoryView; |
| 180 |
| 181 // The JS manager for interacting with the underlying form. |
| 182 base::scoped_nsobject<JsSuggestionManager> _JSSuggestionManager; |
| 183 |
| 184 // The original subviews in keyboard accessory view that were originally not |
| 185 // hidden but were hidden when showing Autofill suggestions. |
| 186 base::scoped_nsobject<NSMutableArray> _hiddenOriginalSubviews; |
| 187 |
| 188 // The objects that can provide a custom input accessory view while filling |
| 189 // forms. |
| 190 base::scoped_nsobject<NSArray> _providers; |
| 191 |
| 192 // The object that manages the currently-shown custom accessory view. |
| 193 base::WeakNSProtocol<id<FormInputAccessoryViewProvider>> _currentProvider; |
| 194 } |
| 195 |
| 196 - (instancetype)initWithWebState:(web::WebState*)webState |
| 197 providers:(NSArray*)providers { |
| 198 JsSuggestionManager* suggestionManager = |
| 199 base::mac::ObjCCastStrict<JsSuggestionManager>( |
| 200 [webState->GetJSInjectionReceiver() |
| 201 instanceOfClass:[JsSuggestionManager class]]); |
| 202 return [self initWithWebState:webState |
| 203 JSSuggestionManager:suggestionManager |
| 204 providers:providers]; |
| 205 } |
| 206 |
| 207 - (instancetype)initWithWebState:(web::WebState*)webState |
| 208 JSSuggestionManager:(JsSuggestionManager*)JSSuggestionManager |
| 209 providers:(NSArray*)providers { |
| 210 self = [super init]; |
| 211 if (self) { |
| 212 _JSSuggestionManager.reset([JSSuggestionManager retain]); |
| 213 _hiddenOriginalSubviews.reset([[NSMutableArray alloc] init]); |
| 214 _webStateObserverBridge.reset( |
| 215 new web::WebStateObserverBridge(webState, self)); |
| 216 _providers.reset([providers copy]); |
| 217 // There is no defined relation on the timing of JavaScript events and |
| 218 // keyboard showing up. So it is necessary to listen to the keyboard |
| 219 // notification to make sure the keyboard is updated. |
| 220 [[NSNotificationCenter defaultCenter] |
| 221 addObserver:self |
| 222 selector:@selector(keyboardDidChangeFrame:) |
| 223 name:UIKeyboardDidChangeFrameNotification |
| 224 object:nil]; |
| 225 [[NSNotificationCenter defaultCenter] |
| 226 addObserver:self |
| 227 selector:@selector(keyboardDidHide:) |
| 228 name:UIKeyboardDidHideNotification |
| 229 object:nil]; |
| 230 } |
| 231 return self; |
| 232 } |
| 233 |
| 234 - (void)dealloc { |
| 235 [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| 236 [super dealloc]; |
| 237 } |
| 238 |
| 239 - (web::WebState*)webState { |
| 240 return _webStateObserverBridge ? _webStateObserverBridge->web_state() |
| 241 : nullptr; |
| 242 } |
| 243 |
| 244 - (id<CRWWebViewProxy>)webViewProxy { |
| 245 return self.webState ? self.webState->GetWebViewProxy() : nil; |
| 246 } |
| 247 |
| 248 - (void)hideSubviewsInOriginalAccessoryView:(UIView*)accessoryView { |
| 249 for (UIView* subview in [accessoryView subviews]) { |
| 250 if (!subview.hidden) { |
| 251 [_hiddenOriginalSubviews addObject:subview]; |
| 252 subview.hidden = YES; |
| 253 } |
| 254 } |
| 255 } |
| 256 |
| 257 - (void)showCustomInputAccessoryView:(UIView*)view { |
| 258 [self restoreDefaultInputAccessoryView]; |
| 259 CGRect leftFrame; |
| 260 CGRect rightFrame; |
| 261 UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory]; |
| 262 if (ComputeFramesOfKeyboardParts(inputAccessoryView, &leftFrame, |
| 263 &rightFrame)) { |
| 264 [self hideSubviewsInOriginalAccessoryView:inputAccessoryView]; |
| 265 _customAccessoryView.reset( |
| 266 [[FormInputAccessoryView alloc] initWithFrame:inputAccessoryView.frame |
| 267 delegate:self |
| 268 customView:view |
| 269 leftFrame:leftFrame |
| 270 rightFrame:rightFrame]); |
| 271 [inputAccessoryView addSubview:_customAccessoryView]; |
| 272 } |
| 273 } |
| 274 |
| 275 - (void)restoreDefaultInputAccessoryView { |
| 276 [_customAccessoryView removeFromSuperview]; |
| 277 _customAccessoryView.reset(); |
| 278 for (UIView* subview in _hiddenOriginalSubviews.get()) { |
| 279 subview.hidden = NO; |
| 280 } |
| 281 [_hiddenOriginalSubviews removeAllObjects]; |
| 282 } |
| 283 |
| 284 - (void)closeKeyboard { |
| 285 BOOL performedAction = |
| 286 [self executeFormAssistAction:ios_internal::autofill:: |
| 287 kFormSuggestionAssistButtonDone]; |
| 288 |
| 289 if (!performedAction) { |
| 290 // We could not find the built-in form assist controls, so try to focus |
| 291 // the next or previous control using JavaScript. |
| 292 [self runBlockAllowingKeyboardDisplay:^{ |
| 293 [_JSSuggestionManager closeKeyboard]; |
| 294 }]; |
| 295 } |
| 296 } |
| 297 |
| 298 - (BOOL)executeFormAssistAction:(NSString*)actionName { |
| 299 UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory]; |
| 300 if (!inputAccessoryView) |
| 301 return NO; |
| 302 |
| 303 NSArray* descendants = |
| 304 FindDescendantToolbarItemsForActionName(inputAccessoryView, actionName); |
| 305 |
| 306 if (![descendants count]) |
| 307 return NO; |
| 308 |
| 309 UIBarButtonItem* item = descendants[0]; |
| 310 [[item target] performSelector:[item action] withObject:item]; |
| 311 return YES; |
| 312 } |
| 313 |
| 314 - (void)runBlockAllowingKeyboardDisplay:(ProceduralBlock)block { |
| 315 DCHECK([UIWebView |
| 316 instancesRespondToSelector:@selector(keyboardDisplayRequiresUserAction)]); |
| 317 |
| 318 BOOL originalValue = [self.webViewProxy keyboardDisplayRequiresUserAction]; |
| 319 [self.webViewProxy setKeyboardDisplayRequiresUserAction:NO]; |
| 320 block(); |
| 321 [self.webViewProxy setKeyboardDisplayRequiresUserAction:originalValue]; |
| 322 } |
| 323 |
| 324 #pragma mark - |
| 325 #pragma mark FormInputAccessoryViewDelegate |
| 326 |
| 327 - (void)selectPreviousElement { |
| 328 BOOL performedAction = [self |
| 329 executeFormAssistAction:ios_internal::autofill:: |
| 330 kFormSuggestionAssistButtonPreviousElement]; |
| 331 if (!performedAction) { |
| 332 // We could not find the built-in form assist controls, so try to focus |
| 333 // the next or previous control using JavaScript. |
| 334 [self runBlockAllowingKeyboardDisplay:^{ |
| 335 [_JSSuggestionManager selectPreviousElement]; |
| 336 }]; |
| 337 } |
| 338 } |
| 339 |
| 340 - (void)selectNextElement { |
| 341 BOOL performedAction = |
| 342 [self executeFormAssistAction:ios_internal::autofill:: |
| 343 kFormSuggestionAssistButtonNextElement]; |
| 344 |
| 345 if (!performedAction) { |
| 346 // We could not find the built-in form assist controls, so try to focus |
| 347 // the next or previous control using JavaScript. |
| 348 [self runBlockAllowingKeyboardDisplay:^{ |
| 349 [_JSSuggestionManager selectNextElement]; |
| 350 }]; |
| 351 } |
| 352 } |
| 353 |
| 354 - (void)fetchPreviousAndNextElementsPresenceWithCompletionHandler: |
| 355 (void (^)(BOOL, BOOL))completionHandler { |
| 356 DCHECK(completionHandler); |
| 357 [_JSSuggestionManager |
| 358 fetchPreviousAndNextElementsPresenceWithCompletionHandler: |
| 359 completionHandler]; |
| 360 } |
| 361 |
| 362 #pragma mark - |
| 363 #pragma mark CRWWebStateObserver |
| 364 |
| 365 - (void)pageLoaded:(web::WebState*)webState { |
| 366 [self reset]; |
| 367 } |
| 368 |
| 369 - (void)formActivity:(web::WebState*)webState |
| 370 formName:(const std::string&)formName |
| 371 fieldName:(const std::string&)fieldName |
| 372 type:(const std::string&)type |
| 373 value:(const std::string&)value |
| 374 keyCode:(int)keyCode |
| 375 error:(BOOL)error { |
| 376 web::URLVerificationTrustLevel trustLevel; |
| 377 const GURL pageURL(webState->GetCurrentURL(&trustLevel)); |
| 378 if (error || trustLevel != web::URLVerificationTrustLevel::kAbsolute || |
| 379 !web::UrlHasWebScheme(pageURL) || !webState->ContentIsHTML()) { |
| 380 [self reset]; |
| 381 return; |
| 382 } |
| 383 |
| 384 if ((type == "blur" || type == "change")) { |
| 385 return; |
| 386 } |
| 387 |
| 388 [self retrieveAccessoryViewForForm:formName |
| 389 field:fieldName |
| 390 value:value |
| 391 type:type |
| 392 webState:webState]; |
| 393 } |
| 394 |
| 395 - (void)webStateDestroyed:(web::WebState*)webState { |
| 396 [self reset]; |
| 397 _webStateObserverBridge.reset(); |
| 398 } |
| 399 |
| 400 - (void)reset { |
| 401 if (_currentProvider) { |
| 402 [_currentProvider inputAccessoryViewControllerDidReset:self]; |
| 403 _currentProvider.reset(); |
| 404 } |
| 405 [self restoreDefaultInputAccessoryView]; |
| 406 } |
| 407 |
| 408 - (void)retrieveAccessoryViewForForm:(const std::string&)formName |
| 409 field:(const std::string&)fieldName |
| 410 value:(const std::string&)value |
| 411 type:(const std::string&)type |
| 412 webState:(web::WebState*)webState { |
| 413 base::WeakNSObject<FormInputAccessoryViewController> weakSelf(self); |
| 414 std::string strongFormName = formName; |
| 415 std::string strongFieldName = fieldName; |
| 416 std::string strongValue = value; |
| 417 std::string strongType = type; |
| 418 |
| 419 // Build a block for each provider that will invoke its completion with YES |
| 420 // if the provider can provide an accessory view for the specified form/field |
| 421 // and NO otherwise. |
| 422 base::scoped_nsobject<NSMutableArray> findProviderBlocks( |
| 423 [[NSMutableArray alloc] init]); |
| 424 for (NSUInteger i = 0; i < [_providers count]; i++) { |
| 425 base::mac::ScopedBlock<passwords::PipelineBlock> block( |
| 426 ^(void (^completion)(BOOL success)) { |
| 427 // Access all the providers through |self| to guarantee that both |
| 428 // |self| and all the providers exist when the block is executed. |
| 429 // |_providers| is immutable, so the subscripting is always valid. |
| 430 base::scoped_nsobject<FormInputAccessoryViewController> strongSelf( |
| 431 [weakSelf retain]); |
| 432 if (!strongSelf) |
| 433 return; |
| 434 id<FormInputAccessoryViewProvider> provider = |
| 435 strongSelf.get()->_providers[i]; |
| 436 [provider checkIfAccessoryViewAvailableForFormNamed:strongFormName |
| 437 fieldName:strongFieldName |
| 438 webState:webState |
| 439 completionHandler:completion]; |
| 440 }, |
| 441 base::scoped_policy::RETAIN); |
| 442 [findProviderBlocks addObject:block]; |
| 443 } |
| 444 |
| 445 // Once the view is retrieved, update the UI. |
| 446 AccessoryViewReadyCompletion readyCompletion = |
| 447 ^(UIView* accessoryView, id<FormInputAccessoryViewProvider> provider) { |
| 448 base::scoped_nsobject<FormInputAccessoryViewController> strongSelf( |
| 449 [weakSelf retain]); |
| 450 if (!strongSelf || !strongSelf.get()->_currentProvider) |
| 451 return; |
| 452 DCHECK_EQ(strongSelf.get()->_currentProvider.get(), provider); |
| 453 [provider setAccessoryViewDelegate:strongSelf]; |
| 454 [strongSelf showCustomInputAccessoryView:accessoryView]; |
| 455 }; |
| 456 |
| 457 // Once a provider is found, use it to retrieve the accessory view. |
| 458 passwords::PipelineCompletionBlock onProviderFound = |
| 459 ^(NSUInteger providerIndex) { |
| 460 if (providerIndex == NSNotFound) { |
| 461 [weakSelf reset]; |
| 462 return; |
| 463 } |
| 464 base::scoped_nsobject<FormInputAccessoryViewController> strongSelf( |
| 465 [weakSelf retain]); |
| 466 if (!strongSelf || ![strongSelf webState]) |
| 467 return; |
| 468 id<FormInputAccessoryViewProvider> provider = |
| 469 strongSelf.get()->_providers[providerIndex]; |
| 470 [strongSelf.get()->_currentProvider |
| 471 inputAccessoryViewControllerDidReset:self]; |
| 472 strongSelf.get()->_currentProvider.reset(provider); |
| 473 [strongSelf.get()->_currentProvider |
| 474 retrieveAccessoryViewForFormNamed:strongFormName |
| 475 fieldName:strongFieldName |
| 476 value:strongValue |
| 477 type:strongType |
| 478 webState:webState |
| 479 completionHandler:readyCompletion]; |
| 480 }; |
| 481 |
| 482 // Run all the blocks in |findProviderBlocks| until one invokes its |
| 483 // completion with YES. The first one to do so will be passed to |
| 484 // |onProviderFound|. |
| 485 passwords::RunSearchPipeline(findProviderBlocks, onProviderFound); |
| 486 } |
| 487 |
| 488 - (void)keyboardDidChangeFrame:(NSNotification*)notification { |
| 489 if (!self.webState || !_currentProvider) |
| 490 return; |
| 491 CGRect keyboardFrame = |
| 492 [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; |
| 493 // With iOS8 (beta) this method can be called even when the rect has not |
| 494 // changed. When this is detected we exit early. |
| 495 if (CGRectEqualToRect(CGRectIntegral(_keyboardFrame), |
| 496 CGRectIntegral(keyboardFrame))) { |
| 497 return; |
| 498 } |
| 499 _keyboardFrame = keyboardFrame; |
| 500 [_currentProvider resizeAccessoryView]; |
| 501 } |
| 502 |
| 503 - (void)keyboardDidHide:(NSNotification*)notification { |
| 504 _keyboardFrame = CGRectZero; |
| 505 } |
| 506 |
| 507 @end |
OLD | NEW |