chromium/ios/chrome/open_extension/open_view_controller.mm

// 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