OLD | NEW |
(Empty) | |
| 1 // Copyright 2016 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/app/application_delegate/user_activity_handler.h" |
| 6 |
| 7 #import <CoreSpotlight/CoreSpotlight.h> |
| 8 #import <UIKit/UIKit.h> |
| 9 |
| 10 #include "base/ios/block_types.h" |
| 11 #include "base/ios/ios_util.h" |
| 12 #include "base/ios/weak_nsobject.h" |
| 13 #include "base/mac/foundation_util.h" |
| 14 #include "base/metrics/histogram_macros.h" |
| 15 #include "base/metrics/user_metrics_action.h" |
| 16 #include "base/strings/sys_string_conversions.h" |
| 17 #include "components/handoff/handoff_utility.h" |
| 18 #import "ios/chrome/app/application_delegate/startup_information.h" |
| 19 #import "ios/chrome/app/application_delegate/tab_opening.h" |
| 20 #include "ios/chrome/app/application_mode.h" |
| 21 #import "ios/chrome/app/spotlight/actions_spotlight_manager.h" |
| 22 #import "ios/chrome/app/spotlight/spotlight_util.h" |
| 23 #include "ios/chrome/browser/app_startup_parameters.h" |
| 24 #include "ios/chrome/browser/chrome_url_constants.h" |
| 25 #include "ios/chrome/browser/experimental_flags.h" |
| 26 #include "ios/chrome/browser/metrics/first_user_action_recorder.h" |
| 27 #import "ios/chrome/browser/tabs/tab.h" |
| 28 #import "ios/chrome/browser/tabs/tab_model.h" |
| 29 #import "ios/chrome/browser/u2f/u2f_controller.h" |
| 30 #import "ios/chrome/browser/ui/main/browser_view_information.h" |
| 31 #import "net/base/mac/url_conversions.h" |
| 32 #include "ui/base/page_transition_types.h" |
| 33 #include "url/gurl.h" |
| 34 |
| 35 using base::UserMetricsAction; |
| 36 |
| 37 namespace { |
| 38 // Constants for 3D touch application static shortcuts. |
| 39 NSString* const kShortcutNewTab = @"OpenNewTab"; |
| 40 NSString* const kShortcutNewIncognitoTab = @"OpenIncognitoTab"; |
| 41 NSString* const kShortcutVoiceSearch = @"OpenVoiceSearch"; |
| 42 NSString* const kShortcutQRScanner = @"OpenQRScanner"; |
| 43 } // namespace |
| 44 |
| 45 @interface UserActivityHandler () |
| 46 // Handles the 3D touch application static items. Does nothing if in first run. |
| 47 + (BOOL)handleShortcutItem:(UIApplicationShortcutItem*)shortcutItem |
| 48 startupInformation:(id<StartupInformation>)startupInformation; |
| 49 // Routes Universal 2nd Factor (U2F) callback to the correct Tab. |
| 50 + (void)routeU2FURL:(const GURL&)URL |
| 51 browserViewInformation:(id<BrowserViewInformation>)browserViewInformation; |
| 52 @end |
| 53 |
| 54 @implementation UserActivityHandler |
| 55 |
| 56 #pragma mark - Public methods. |
| 57 |
| 58 + (BOOL)continueUserActivity:(NSUserActivity*)userActivity |
| 59 applicationIsActive:(BOOL)applicationIsActive |
| 60 tabOpener:(id<TabOpening>)tabOpener |
| 61 startupInformation:(id<StartupInformation>)startupInformation { |
| 62 NSURL* webpageURL = userActivity.webpageURL; |
| 63 |
| 64 if ([userActivity.activityType |
| 65 isEqualToString:handoff::kChromeHandoffActivityType]) { |
| 66 // App was launched by iOS as a result of Handoff. |
| 67 NSString* originString = base::mac::ObjCCast<NSString>( |
| 68 userActivity.userInfo[handoff::kOriginKey]); |
| 69 handoff::Origin origin = handoff::OriginFromString(originString); |
| 70 UMA_HISTOGRAM_ENUMERATION("IOS.Handoff.Origin", origin, |
| 71 handoff::ORIGIN_COUNT); |
| 72 } else if ([userActivity.activityType |
| 73 isEqualToString:NSUserActivityTypeBrowsingWeb]) { |
| 74 // App was launched as the result of a Universal Link navigation. The value |
| 75 // of userActivity.webpageURL is not used. The only supported action |
| 76 // at this time is opening a New Tab Page. |
| 77 GURL newTabURL(kChromeUINewTabURL); |
| 78 webpageURL = net::NSURLWithGURL(newTabURL); |
| 79 base::scoped_nsobject<AppStartupParameters> startupParams( |
| 80 [[AppStartupParameters alloc] initWithExternalURL:newTabURL]); |
| 81 [startupInformation setStartupParameters:startupParams]; |
| 82 base::RecordAction(base::UserMetricsAction("IOSLaunchedByUniversalLink")); |
| 83 } else if (spotlight::IsSpotlightAvailable() && |
| 84 [userActivity.activityType |
| 85 isEqualToString:CSSearchableItemActionType]) { |
| 86 // App was launched by iOS as the result of a tap on a Spotlight Search |
| 87 // result. |
| 88 NSString* itemID = |
| 89 [userActivity.userInfo objectForKey:CSSearchableItemActivityIdentifier]; |
| 90 spotlight::Domain domain = spotlight::SpotlightDomainFromString(itemID); |
| 91 if (domain == spotlight::DOMAIN_ACTIONS && |
| 92 !experimental_flags::IsSpotlightActionsEnabled()) { |
| 93 return NO; |
| 94 } |
| 95 UMA_HISTOGRAM_ENUMERATION("IOS.Spotlight.Origin", domain, |
| 96 spotlight::DOMAIN_COUNT); |
| 97 |
| 98 if (!itemID) { |
| 99 return NO; |
| 100 } |
| 101 if (domain == spotlight::DOMAIN_ACTIONS) { |
| 102 webpageURL = |
| 103 [NSURL URLWithString:base::SysUTF8ToNSString(kChromeUINewTabURL)]; |
| 104 base::scoped_nsobject<AppStartupParameters> startupParams( |
| 105 [[AppStartupParameters alloc] |
| 106 initWithExternalURL:GURL(kChromeUINewTabURL)]); |
| 107 BOOL startupParamsSet = spotlight::SetStartupParametersForSpotlightAction( |
| 108 itemID, startupParams); |
| 109 if (!startupParamsSet) { |
| 110 return NO; |
| 111 } |
| 112 [startupInformation setStartupParameters:startupParams]; |
| 113 } else if (!webpageURL && base::ios::IsRunningOnIOS10OrLater()) { |
| 114 // spotlight::GetURLForSpotlightItemID uses CSSearchQuery, which is only |
| 115 // supported from iOS 10. |
| 116 spotlight::GetURLForSpotlightItemID(itemID, ^(NSURL* contentURL) { |
| 117 if (!contentURL) { |
| 118 return; |
| 119 } |
| 120 dispatch_async(dispatch_get_main_queue(), ^{ |
| 121 // Update the isActive flag as it may have changed during the async |
| 122 // calls. |
| 123 BOOL isActive = [[UIApplication sharedApplication] |
| 124 applicationState] == UIApplicationStateActive; |
| 125 [self continueUserActivityURL:contentURL |
| 126 applicationIsActive:isActive |
| 127 tabOpener:tabOpener |
| 128 startupInformation:startupInformation]; |
| 129 }); |
| 130 }); |
| 131 return YES; |
| 132 } |
| 133 } else { |
| 134 // Do nothing for unknown activity type. |
| 135 return NO; |
| 136 } |
| 137 |
| 138 return [self continueUserActivityURL:webpageURL |
| 139 applicationIsActive:applicationIsActive |
| 140 tabOpener:tabOpener |
| 141 startupInformation:startupInformation]; |
| 142 } |
| 143 |
| 144 + (BOOL)continueUserActivityURL:(NSURL*)webpageURL |
| 145 applicationIsActive:(BOOL)applicationIsActive |
| 146 tabOpener:(id<TabOpening>)tabOpener |
| 147 startupInformation:(id<StartupInformation>)startupInformation { |
| 148 if (!webpageURL) |
| 149 return NO; |
| 150 |
| 151 GURL webpageGURL(net::GURLWithNSURL(webpageURL)); |
| 152 if (!webpageGURL.is_valid()) |
| 153 return NO; |
| 154 |
| 155 if (applicationIsActive && ![startupInformation isPresentingFirstRunUI]) { |
| 156 // The app is already active so the applicationDidBecomeActive: method will |
| 157 // never be called. Open the requested URL immediately. |
| 158 ApplicationMode targetMode = |
| 159 [[startupInformation startupParameters] launchInIncognito] |
| 160 ? ApplicationMode::INCOGNITO |
| 161 : ApplicationMode::NORMAL; |
| 162 [tabOpener dismissModalsAndOpenSelectedTabInMode:targetMode |
| 163 withURL:webpageGURL |
| 164 transition:ui::PAGE_TRANSITION_LINK |
| 165 completion:^{ |
| 166 [startupInformation |
| 167 setStartupParameters:nil]; |
| 168 }]; |
| 169 return YES; |
| 170 } |
| 171 |
| 172 // Don't record the first action as a user action, since it will not be |
| 173 // initiated by the user. |
| 174 [startupInformation resetFirstUserActionRecorder]; |
| 175 |
| 176 if (![startupInformation startupParameters]) { |
| 177 base::scoped_nsobject<AppStartupParameters> startupParams( |
| 178 [[AppStartupParameters alloc] initWithExternalURL:webpageGURL]); |
| 179 [startupInformation setStartupParameters:startupParams]; |
| 180 } |
| 181 return YES; |
| 182 } |
| 183 |
| 184 + (void)performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem |
| 185 completionHandler:(void (^)(BOOL succeeded))completionHandler |
| 186 tabOpener:(id<TabOpening>)tabOpener |
| 187 startupInformation:(id<StartupInformation>)startupInformation |
| 188 browserViewInformation: |
| 189 (id<BrowserViewInformation>)browserViewInformation { |
| 190 BOOL handledShortcutItem = |
| 191 [UserActivityHandler handleShortcutItem:shortcutItem |
| 192 startupInformation:startupInformation]; |
| 193 if (handledShortcutItem) { |
| 194 [UserActivityHandler |
| 195 handleStartupParametersWithTabOpener:tabOpener |
| 196 startupInformation:startupInformation |
| 197 browserViewInformation:browserViewInformation]; |
| 198 } |
| 199 completionHandler(handledShortcutItem); |
| 200 } |
| 201 |
| 202 + (BOOL)willContinueUserActivityWithType:(NSString*)userActivityType { |
| 203 return |
| 204 [userActivityType isEqualToString:handoff::kChromeHandoffActivityType] || |
| 205 (spotlight::IsSpotlightAvailable() && |
| 206 [userActivityType isEqualToString:CSSearchableItemActionType]); |
| 207 } |
| 208 |
| 209 + (void)handleStartupParametersWithTabOpener:(id<TabOpening>)tabOpener |
| 210 startupInformation: |
| 211 (id<StartupInformation>)startupInformation |
| 212 browserViewInformation: |
| 213 (id<BrowserViewInformation>)browserViewInformation { |
| 214 DCHECK([startupInformation startupParameters]); |
| 215 // Do not load the external URL if the user has not accepted the terms of |
| 216 // service. This corresponds to the case when the user installed Chrome, |
| 217 // has never launched it and attempts to open an external URL in Chrome. |
| 218 if ([startupInformation isPresentingFirstRunUI]) |
| 219 return; |
| 220 |
| 221 // Check if it's an U2F call. If so, route it to correct tab. |
| 222 // If not, open or reuse tab in main BVC. |
| 223 if ([U2FController |
| 224 isU2FURL:[[startupInformation startupParameters] externalURL]]) { |
| 225 [UserActivityHandler routeU2FURL:[[startupInformation startupParameters] |
| 226 externalURL] |
| 227 browserViewInformation:browserViewInformation]; |
| 228 // It's OK to clear startup parameters here because routeU2FURL works |
| 229 // synchronously. |
| 230 [startupInformation setStartupParameters:nil]; |
| 231 } else { |
| 232 // The app is already active so the applicationDidBecomeActive: method |
| 233 // will never be called. Open the requested URL after all modal UIs have |
| 234 // been dismissed. |_startupParameters| must be retained until all deferred |
| 235 // modal UIs are dismissed and tab opened with requested URL. |
| 236 ApplicationMode targetMode = |
| 237 [[startupInformation startupParameters] launchInIncognito] |
| 238 ? ApplicationMode::INCOGNITO |
| 239 : ApplicationMode::NORMAL; |
| 240 [tabOpener dismissModalsAndOpenSelectedTabInMode:targetMode |
| 241 withURL:[[startupInformation |
| 242 startupParameters] |
| 243 externalURL] |
| 244 transition:ui::PAGE_TRANSITION_LINK |
| 245 completion:^{ |
| 246 [startupInformation |
| 247 setStartupParameters:nil]; |
| 248 }]; |
| 249 } |
| 250 } |
| 251 |
| 252 #pragma mark - Internal methods. |
| 253 |
| 254 + (BOOL)handleShortcutItem:(UIApplicationShortcutItem*)shortcutItem |
| 255 startupInformation:(id<StartupInformation>)startupInformation { |
| 256 if ([startupInformation isPresentingFirstRunUI]) |
| 257 return NO; |
| 258 |
| 259 base::scoped_nsobject<AppStartupParameters> startupParams( |
| 260 [[AppStartupParameters alloc] |
| 261 initWithExternalURL:GURL(kChromeUINewTabURL)]); |
| 262 |
| 263 if ([shortcutItem.type isEqualToString:kShortcutNewTab]) { |
| 264 base::RecordAction(UserMetricsAction("ApplicationShortcut.NewTabPressed")); |
| 265 [startupInformation setStartupParameters:startupParams]; |
| 266 return YES; |
| 267 |
| 268 } else if ([shortcutItem.type isEqualToString:kShortcutNewIncognitoTab]) { |
| 269 base::RecordAction( |
| 270 UserMetricsAction("ApplicationShortcut.NewIncognitoTabPressed")); |
| 271 [startupParams setLaunchInIncognito:YES]; |
| 272 [startupInformation setStartupParameters:startupParams]; |
| 273 return YES; |
| 274 |
| 275 } else if ([shortcutItem.type isEqualToString:kShortcutVoiceSearch]) { |
| 276 base::RecordAction( |
| 277 UserMetricsAction("ApplicationShortcut.VoiceSearchPressed")); |
| 278 [startupParams setLaunchVoiceSearch:YES]; |
| 279 [startupInformation setStartupParameters:startupParams]; |
| 280 return YES; |
| 281 |
| 282 } else if ([shortcutItem.type isEqualToString:kShortcutQRScanner]) { |
| 283 if (experimental_flags::IsQRCodeReaderEnabled()) { |
| 284 base::RecordAction( |
| 285 UserMetricsAction("ApplicationShortcut.ScanQRCodePressed")); |
| 286 [startupParams setLaunchQRScanner:YES]; |
| 287 } |
| 288 [startupInformation setStartupParameters:startupParams]; |
| 289 return YES; |
| 290 } |
| 291 |
| 292 NOTREACHED(); |
| 293 return NO; |
| 294 } |
| 295 |
| 296 + (void)routeU2FURL:(const GURL&)URL |
| 297 browserViewInformation:(id<BrowserViewInformation>)browserViewInformation { |
| 298 // Retrieve the designated TabID from U2F URL. |
| 299 NSString* tabID = [U2FController tabIDFromResponseURL:URL]; |
| 300 if (!tabID) { |
| 301 return; |
| 302 } |
| 303 |
| 304 // TODO(crbug.com/619598): move this code to BrowserViewInformation to hide |
| 305 // implementation details of TabModel. |
| 306 // Iterate through mainTabModel and OTRTabModel to find the corresponding tab. |
| 307 NSArray* tabModels = @[ |
| 308 [browserViewInformation mainTabModel], [browserViewInformation otrTabModel] |
| 309 ]; |
| 310 for (TabModel* tabModel in tabModels) { |
| 311 for (Tab* tab in tabModel) { |
| 312 if ([tab.tabId isEqualToString:tabID]) { |
| 313 [tab evaluateU2FResultFromURL:URL]; |
| 314 return; |
| 315 } |
| 316 } |
| 317 } |
| 318 } |
| 319 |
| 320 @end |
OLD | NEW |