| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/open_extension/open_view_controller.h" |
| |
| #import <UniformTypeIdentifiers/UniformTypeIdentifiers.h> |
| |
| #import "base/apple/bundle_locations.h" |
| #import "base/apple/foundation_util.h" |
| #import "base/ios/block_types.h" |
| #import "base/notreached.h" |
| #import "base/strings/sys_string_conversions.h" |
| #import "ios/chrome/common/app_group/app_group_command.h" |
| #import "ios/chrome/common/app_group/app_group_constants.h" |
| #import "ios/chrome/common/extension_open_url.h" |
| |
| // Type for completion handler to fetch the components of the share items. |
| // `idResponse` type depends on the element beeing fetched. |
| using ItemBlock = void (^)(id idResponse, NSError* error); |
| |
| namespace { |
| |
| // Logs the new outcome by incrementing the outcome dictionary's values. |
| void LogOutcome(app_group::OpenExtensionOutcome outcome_type) { |
| NSUserDefaults* shared_defaults = app_group::GetGroupUserDefaults(); |
| |
| NSMutableDictionary<NSString*, NSNumber*>* |
| open_extension_outcome_dictionnary = [[shared_defaults |
| dictionaryForKey:app_group::kOpenExtensionOutcomes] mutableCopy]; |
| |
| if (!open_extension_outcome_dictionnary) { |
| open_extension_outcome_dictionnary = [NSMutableDictionary dictionary]; |
| } |
| |
| NSString* key_for_outcome_type = KeyForOpenExtensionOutcomeType(outcome_type); |
| |
| NSInteger old_value_for_open_in_outcome = |
| open_extension_outcome_dictionnary[key_for_outcome_type].integerValue; |
| |
| [open_extension_outcome_dictionnary |
| setValue:@(old_value_for_open_in_outcome + 1) |
| forKey:key_for_outcome_type]; |
| |
| [shared_defaults setObject:open_extension_outcome_dictionnary |
| forKey:app_group::kOpenExtensionOutcomes]; |
| [shared_defaults synchronize]; |
| } |
| |
| // Convert outcome_type to an error type. |
| NSError* ErrorForOutcome(app_group::OpenExtensionOutcome outcome_type) { |
| NSInteger error_code = NSURLErrorUnknown; |
| switch (outcome_type) { |
| case app_group::OpenExtensionOutcome::kFailureInvalidURL: |
| error_code = NSURLErrorBadURL; |
| break; |
| case app_group::OpenExtensionOutcome::kFailureURLNotFound: |
| error_code = NSURLErrorBadURL; |
| break; |
| case app_group::OpenExtensionOutcome::kFailureOpenInNotFound: |
| error_code = NSURLErrorUnknown; |
| break; |
| case app_group::OpenExtensionOutcome::kFailureUnsupportedScheme: |
| error_code = NSURLErrorUnsupportedURL; |
| break; |
| default: |
| NOTREACHED(); |
| } |
| return [NSError errorWithDomain:NSURLErrorDomain |
| code:error_code |
| userInfo:nil]; |
| } |
| } // namespace |
| |
| @implementation OpenViewController { |
| NSURL* _openInURL; |
| NSExtensionItem* _openInItem; |
| } |
| |
| #pragma mark - UIViewController |
| |
| - (void)viewDidLoad { |
| [super viewDidLoad]; |
| [self loadElementsFromContext]; |
| } |
| |
| - (void)loadElementsFromContext { |
| NSString* typeURL = UTTypeURL.identifier; |
| BOOL foundMatch = false; |
| for (NSExtensionItem* item in self.extensionContext.inputItems) { |
| for (NSItemProvider* itemProvider in item.attachments) { |
| if ([itemProvider hasItemConformingToTypeIdentifier:typeURL]) { |
| foundMatch = true; |
| __weak __typeof(self) weakSelf = self; |
| ItemBlock URLCompletion = ^(id idURL, NSError* error) { |
| NSURL* URL = base::apple::ObjCCast<NSURL>(idURL); |
| if (!URL) { |
| // Display the error view when the URL is invalid. |
| [self displayErrorViewForOutcome:app_group::OpenExtensionOutcome:: |
| kFailureInvalidURL]; |
| return; |
| } |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [weakSelf shareItem:item url:URL]; |
| }); |
| }; |
| [itemProvider loadItemForTypeIdentifier:typeURL |
| options:nil |
| completionHandler:URLCompletion]; |
| } |
| } |
| } |
| // Display the error view when no URL has been found. |
| if (!foundMatch) { |
| [self displayErrorViewForOutcome:app_group::OpenExtensionOutcome:: |
| kFailureURLNotFound]; |
| } |
| } |
| |
| - (void)shareItem:(NSExtensionItem*)item url:(NSURL*)URL { |
| _openInItem = [item copy]; |
| _openInURL = [URL copy]; |
| if ([[_openInURL scheme] isEqualToString:@"http"] || |
| [[_openInURL scheme] isEqualToString:@"https"]) { |
| [self openInChrome]; |
| } else { |
| [self displayErrorViewForOutcome:app_group::OpenExtensionOutcome:: |
| kFailureUnsupportedScheme]; |
| } |
| } |
| |
| - (void)performOpenURL:(NSURL*)openURL { |
| bool result = ExtensionOpenURL(openURL, self, ^(BOOL success) { |
| if (success) { |
| LogOutcome(app_group::OpenExtensionOutcome::kSuccess); |
| } |
| }); |
| if (result) { |
| [self.extensionContext completeRequestReturningItems:@[ _openInItem ] |
| completionHandler:nil]; |
| return; |
| } |
| // Display the error view when Open in is not found |
| [self displayErrorViewForOutcome:app_group::OpenExtensionOutcome:: |
| kFailureOpenInNotFound]; |
| } |
| |
| - (void)openInChrome { |
| __weak OpenViewController* weakSelf = self; |
| AppGroupCommand* command = [[AppGroupCommand alloc] |
| initWithSourceApp:app_group::kOpenCommandSourceOpenExtension |
| URLOpenerBlock:^(NSURL* openURL) { |
| [weakSelf performOpenURL:openURL]; |
| }]; |
| [command prepareToOpenURL:_openInURL]; |
| [command executeInApp]; |
| } |
| |
| - (void)displayErrorViewForOutcome:(app_group::OpenExtensionOutcome)outcome { |
| __weak OpenViewController* weakSelf = self; |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [weakSelf displayErrorViewMainThreadForOutcome:outcome]; |
| }); |
| } |
| |
| - (void)displayErrorViewMainThreadForOutcome: |
| (app_group::OpenExtensionOutcome)outcome { |
| LogOutcome(outcome); |
| NSString* errorMessage = |
| NSLocalizedString(@"IDS_IOS_ERROR_MESSAGE_OPEN_IN_EXTENSION", |
| @"The error message to display to the user."); |
| NSString* okButton = |
| NSLocalizedString(@"IDS_IOS_OK_BUTTON_OPEN_IN_EXTENSION", |
| @"The label of the OK button in open in extension."); |
| UIAlertController* alert = |
| [UIAlertController alertControllerWithTitle:errorMessage |
| message:[_openInURL absoluteString] |
| preferredStyle:UIAlertControllerStyleAlert]; |
| |
| __weak __typeof(self) weakSelf = self; |
| UIAlertAction* defaultAction = [UIAlertAction |
| actionWithTitle:okButton |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| NSError* outcomeError = ErrorForOutcome(outcome); |
| [weakSelf.extensionContext cancelRequestWithError:outcomeError]; |
| }]; |
| [alert addAction:defaultAction]; |
| [self presentViewController:alert animated:YES completion:nil]; |
| } |
| @end |