chromium/ios/chrome/browser/shared/ui/util/image/image_saver.mm

// Copyright 2017 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/browser/shared/ui/util/image/image_saver.h"

#import <Photos/Photos.h>

#import "base/feature_list.h"
#import "base/files/file_path.h"
#import "base/format_macros.h"
#import "base/functional/bind.h"
#import "base/ios/ios_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/thread_pool.h"
#import "base/threading/scoped_blocking_call.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/shared/coordinator/alert/alert_coordinator.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/ui/util/image/image_util.h"
#import "ios/chrome/browser/web/model/image_fetch/image_fetch_tab_helper.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "net/base/mime_util.h"
#import "ui/base/l10n/l10n_util.h"

@interface ImageSaver ()
// Base view controller for the alerts.
@property(nonatomic, weak) UIViewController* baseViewController;
// Alert coordinator to give feedback to the user.
@property(nonatomic, strong) AlertCoordinator* alertCoordinator;
@property(nonatomic, readonly) Browser* browser;
@end

@implementation ImageSaver

- (instancetype)initWithBrowser:(Browser*)browser {
  self = [super init];
  if (self) {
    _browser = browser;
  }
  return self;
}

- (void)stop {
  [self.alertCoordinator stop];
  self.alertCoordinator = nil;
  self.baseViewController = nil;
  _browser = nullptr;
}

- (void)saveImageAtURL:(const GURL&)URL
              referrer:(const web::Referrer&)referrer
              webState:(web::WebState*)webState
    baseViewController:(UIViewController*)baseViewController {
  self.baseViewController = baseViewController;

  ImageFetchTabHelper* tabHelper = ImageFetchTabHelper::FromWebState(webState);
  DCHECK(tabHelper);

  __weak ImageSaver* weakSelf = self;
  tabHelper->GetImageData(URL, referrer, ^(NSData* data) {
    [weakSelf didGetImageData:data];
  });
}

// Callback when the image `data` got retrieved from the tab.
- (void)didGetImageData:(NSData*)data {
  if (data.length == 0) {
    [self
        displayPrivacyErrorAlertOnMainQueue:
            l10n_util::GetNSString(IDS_IOS_SAVE_IMAGE_NO_INTERNET_CONNECTION)];
    return;
  }

  // Use -imageWithData to validate `data`, but continue to pass the raw
  // `data` to -savePhoto to ensure no data loss occurs.
  UIImage* savedImage = [UIImage imageWithData:data];
  if (!savedImage) {
    [self displayPrivacyErrorAlertOnMainQueue:l10n_util::GetNSString(
                                                  IDS_IOS_SAVE_IMAGE_ERROR)];
    return;
  }

  // Dump `data` into the photo library. Requires the usage of
  // NSPhotoLibraryAddUsageDescription.
  __weak ImageSaver* weakSelf = self;
  [[PHPhotoLibrary sharedPhotoLibrary]
      performChanges:^{
        PHAssetResourceCreationOptions* options =
            [[PHAssetResourceCreationOptions alloc] init];
        [[PHAssetCreationRequest creationRequestForAsset]
            addResourceWithType:PHAssetResourceTypePhoto
                           data:data
                        options:options];
      }
      completionHandler:^(BOOL success, NSError* error) {
        [weakSelf image:savedImage
            didFinishSavingWithError:error
                         contextInfo:nil];
      }];
}

// Called when Chrome has been denied access to add photos or videos and the
// user can change it.
// Shows a privacy alert on the main queue, allowing the user to go to Chrome's
// settings. Dismiss previous alert if it has not been dismissed yet.
- (void)displayImageErrorAlertWithSettingsOnMainQueue {
  __weak ImageSaver* weakSelf = self;
  dispatch_async(dispatch_get_main_queue(), ^{
    NSURL* settingURL =
        [NSURL URLWithString:UIApplicationOpenSettingsURLString];
    BOOL canGoToSetting =
        [[UIApplication sharedApplication] canOpenURL:settingURL];
    if (canGoToSetting) {
      [weakSelf displayImageErrorAlertWithSettings:settingURL];
    } else {
      [weakSelf
          displayPrivacyErrorAlertOnMainQueue:
              l10n_util::GetNSString(IDS_IOS_SAVE_IMAGE_PRIVACY_ALERT_MESSAGE)];
    }
  });
}

// Shows a privacy alert allowing the user to go to Chrome's settings. Dismiss
// previous alert if it has not been dismissed yet.
- (void)displayImageErrorAlertWithSettings:(NSURL*)settingURL {
  // Dismiss current alert.
  [_alertCoordinator stop];

  NSString* title =
      l10n_util::GetNSString(IDS_IOS_SAVE_IMAGE_PRIVACY_ALERT_TITLE);
  NSString* message = l10n_util::GetNSString(
      IDS_IOS_SAVE_IMAGE_PRIVACY_ALERT_MESSAGE_GO_TO_SETTINGS);

  self.alertCoordinator = [[AlertCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:_browser
                           title:title
                         message:message];

  [self.alertCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
                                   action:nil
                                    style:UIAlertActionStyleCancel];

  [_alertCoordinator
      addItemWithTitle:l10n_util::GetNSString(
                           IDS_IOS_SAVE_IMAGE_PRIVACY_ALERT_GO_TO_SETTINGS)
                action:^{
                  [[UIApplication sharedApplication] openURL:settingURL
                                                     options:@{}
                                           completionHandler:nil];
                }
                 style:UIAlertActionStyleDefault];

  [_alertCoordinator start];
}

// Called when Chrome has been denied access to the photos or videos and the
// user cannot change it.
- (void)displayPrivacyErrorAlertOnMainQueue:(NSString*)errorContent {
  __weak ImageSaver* weakSelf = self;
  dispatch_async(dispatch_get_main_queue(), ^{
    [weakSelf asyncDisplayPrivacyErrorAlertOnMainQueue:errorContent];
  });
}

// Async helper implementation of displayPrivacyErrorAlertOnMainQueue.
// Shows a privacy alert on the main queue, with errorContent as the message.
// Dismisses previous alert if it has not been dismissed yet.
- (void)asyncDisplayPrivacyErrorAlertOnMainQueue:(NSString*)errorContent {
  NSString* title =
      l10n_util::GetNSString(IDS_IOS_SAVE_IMAGE_PRIVACY_ALERT_TITLE);
  // Dismiss current alert.
  [self.alertCoordinator stop];

  self.alertCoordinator = [[AlertCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:_browser
                           title:title
                         message:errorContent];
  [self.alertCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_OK)
                                   action:nil
                                    style:UIAlertActionStyleDefault];
  [self.alertCoordinator start];
}

// Called after the system attempts to write the image to the saved photos
// album.
- (void)image:(UIImage*)image
    didFinishSavingWithError:(NSError*)error
                 contextInfo:(void*)contextInfo {
  // Was there an error?
  if (error) {
    // Saving photo failed, likely due to a permissions issue.
    // This code may be execute outside of the main thread. Make sure to display
    // the error on the main thread.
    [self displayImageErrorAlertWithSettingsOnMainQueue];
  } else {
    // TODO(crbug.com/41362123): Provide a way for the user to easily reach the
    // photos app.
  }
}

@end