// Copyright 2016 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/share_extension/share_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/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/crash_report/crash_helper.h"
#import "ios/chrome/common/extension_open_url.h"
#import "ios/chrome/share_extension/share_extension_view.h"
#import "ios/chrome/share_extension/ui_util.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 {
// Minimum size around the widget
const CGFloat kShareExtensionMargin = 15;
const CGFloat kShareExtensionMaxWidth = 390;
// Clip the last separator out of the table view.
const CGFloat kScreenShotWidth = 100;
const CGFloat kScreenShotHeight = 100;
const CGFloat kMediumAlpha = 0.5;
} // namespace
@interface ShareViewController () <ShareExtensionViewActionTarget>
@property(nonatomic, weak) UIView* maskView;
@property(nonatomic, weak) ShareExtensionView* shareView;
@property(nonatomic, assign) app_group::ShareExtensionItemType itemType;
@property(nonatomic, strong) NSExtensionItem* shareItem;
@property(nonatomic, strong) NSURL* shareURL;
@property(nonatomic, copy) NSString* shareTitle;
@property(nonatomic, strong) UIImage* image;
// This constrains the center of the widget to be vertically in the center
// of the the screen. It has to be modified for the appearance and dismissal
// animation.
@property(nonatomic, strong)
NSLayoutConstraint* widgetVerticalPlacementConstraint;
// Creates a files in `app_group::ShareExtensionItemsFolder()` containing a
// serialized NSDictionary.
// If `cancel` is true, `actionType` is ignored.
- (void)queueActionItemURL:(NSURL*)URL
title:(NSString*)title
action:(app_group::ShareExtensionItemType)actionType
cancel:(BOOL)cancel
completion:(ProceduralBlock)completion;
// Loads all the shared elements from the extension context and update the UI.
- (void)loadElementsFromContext;
// Sets constraints to the widget so that margin are at least
// kShareExtensionMargin points and widget width is closest up to
// kShareExtensionMaxWidth points.
- (void)constrainWidgetWidth;
// Displays the normal share view.
- (void)displayShareView;
// Displays an alert to report an error to the user.
- (void)displayErrorView;
@end
@implementation ShareViewController
@synthesize maskView = _maskView;
@synthesize shareView = _shareView;
@synthesize itemType = _itemType;
+ (void)initialize {
if (self == [ShareViewController self]) {
crash_helper::common::StartCrashpad();
}
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
// This view shadows the screen under the share extension.
UIView* maskView = [[UIView alloc] initWithFrame:CGRectZero];
self.maskView = maskView;
// On iOS 13, the default share extension presentation style already has a
// mask behind the view.
self.maskView.hidden = YES;
[self.maskView
setBackgroundColor:[UIColor colorWithWhite:0 alpha:kMediumAlpha]];
[self.view addSubview:self.maskView];
ui_util::ConstrainAllSidesOfViewToView(self.view, self.maskView);
// This view is the main view of the share extension.
ShareExtensionView* shareView =
[[ShareExtensionView alloc] initWithActionTarget:self];
[self setShareView:shareView];
[self.view addSubview:self.shareView];
[self constrainWidgetWidth];
// Position the widget below the screen. It will be slided up with an
// animation.
_widgetVerticalPlacementConstraint =
[shareView.topAnchor constraintEqualToAnchor:self.view.bottomAnchor];
[_widgetVerticalPlacementConstraint setActive:YES];
[[shareView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor]
setActive:YES];
[self.maskView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.shareView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self loadElementsFromContext];
}
#pragma mark - Private methods
- (void)displayShareView {
[self.shareView setTitle:_shareTitle];
[self.shareView setURL:_shareURL];
if (_image) {
[self.shareView setScreenshot:_image];
}
__weak ShareViewController* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
// Center the widget.
[weakSelf.widgetVerticalPlacementConstraint setActive:NO];
weakSelf.widgetVerticalPlacementConstraint =
[weakSelf.shareView.centerYAnchor
constraintEqualToAnchor:self.view.centerYAnchor];
[weakSelf.widgetVerticalPlacementConstraint setActive:YES];
[weakSelf.maskView setAlpha:0];
[UIView animateWithDuration:ui_util::kAnimationDuration
animations:^{
[weakSelf.maskView setAlpha:1];
[weakSelf.view layoutIfNeeded];
}];
});
}
- (void)displayErrorView {
__weak ShareViewController* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf displayErrorViewMainThread];
});
}
- (void)displayErrorViewMainThread {
NSString* errorMessage =
NSLocalizedString(@"IDS_IOS_ERROR_MESSAGE_SHARE_EXTENSION",
@"The error message to display to the user.");
NSString* okButton =
NSLocalizedString(@"IDS_IOS_OK_BUTTON_SHARE_EXTENSION",
@"The label of the OK button in share extension.");
NSString* applicationName = [[base::apple::FrameworkBundle() infoDictionary]
valueForKey:@"CFBundleDisplayName"];
errorMessage =
[errorMessage stringByReplacingOccurrencesOfString:@"APPLICATION_NAME"
withString:applicationName];
UIAlertController* alert =
[UIAlertController alertControllerWithTitle:errorMessage
message:[_shareURL absoluteString]
preferredStyle:UIAlertControllerStyleAlert];
__weak ShareViewController* weakSelf = self;
UIAlertAction* defaultAction = [UIAlertAction
actionWithTitle:okButton
style:UIAlertActionStyleDefault
handler:^(UIAlertAction* action) {
NSError* unsupportedURLError =
[NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorUnsupportedURL
userInfo:nil];
[weakSelf dismissAndReturnItem:nil error:unsupportedURLError];
}];
[alert addAction:defaultAction];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)constrainWidgetWidth {
// Setting the constraints.
NSDictionary* views = @{ @"share" : self.shareView };
NSDictionary* metrics = @{
@"margin" : @(kShareExtensionMargin),
@"maxWidth" : @(kShareExtensionMaxWidth),
};
NSArray* constraints = @[
// Sets the margin around the share extension.
@"H:|-(>=margin)-[share(<=maxWidth)]-(>=margin)-|",
// If the screen is too small, reduce width of widget.
@"H:[share(==maxWidth@900)]",
];
for (NSString* constraint : constraints) {
[NSLayoutConstraint
activateConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:constraint
options:0
metrics:metrics
views:views]];
}
// `self.shareView` must be as large as possible and in the center of the
// screen.
[self.shareView
setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh
forAxis:UILayoutConstraintAxisHorizontal];
}
- (void)handleURL:(id)idURL
forItem:(NSExtensionItem*)item
withError:(NSError*)error {
NSURL* URL = base::apple::ObjCCast<NSURL>(idURL);
if (!URL) {
[self displayErrorView];
return;
}
self.shareItem = item;
self.shareURL = URL;
self.shareTitle = [[item attributedContentText] string];
if ([self.shareTitle length] == 0) {
self.shareTitle = [URL host];
}
if ([[self.shareURL scheme] isEqualToString:@"http"] ||
[[self.shareURL scheme] isEqualToString:@"https"]) {
[self displayShareView];
} else {
[self displayErrorView];
}
}
- (void)handleImage:(id)idImage
forItem:(NSExtensionItem*)item
withError:(NSError*)error {
self.image = base::apple::ObjCCast<UIImage>(idImage);
if (self.image && self.shareView) {
[self.shareView setScreenshot:self.image];
}
}
- (void)loadElementsFromContext {
NSString* typeURL = UTTypeURL.identifier;
__weak ShareViewController* weakSelf = self;
// TODO(crbug.com/40278725): Reorganize sharing extension handler.
BOOL foundMatch = false;
for (NSExtensionItem* item in self.extensionContext.inputItems) {
for (NSItemProvider* itemProvider in item.attachments) {
if ([itemProvider hasItemConformingToTypeIdentifier:typeURL]) {
foundMatch = true;
ItemBlock URLCompletion = ^(id idURL, NSError* error) {
// Crash reports showed that this block can be called on a background
// thread. Move back the UI updating code to main thread.
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf handleURL:idURL forItem:item withError:error];
});
};
[itemProvider loadItemForTypeIdentifier:typeURL
options:nil
completionHandler:URLCompletion];
NSDictionary* imageOptions = @{
NSItemProviderPreferredImageSizeKey : [NSValue
valueWithCGSize:CGSizeMake(kScreenShotWidth, kScreenShotHeight)]
};
ItemBlock imageCompletion = ^(id imageData, NSError* error) {
// Crash reports showed that this block can be called on a background
// thread. Move back the UI updating code to main thread.
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf handleImage:imageData forItem:item withError:error];
});
};
[itemProvider loadPreviewImageWithOptions:imageOptions
completionHandler:imageCompletion];
}
}
}
// Display the error view when no match have been found.
if (!foundMatch) {
[self displayErrorView];
}
}
- (void)dismissAndReturnItem:(NSExtensionItem*)item error:(NSError*)error {
// Set the Y placement constraints so the whole extension slides out of the
// screen.
// The direction (up or down) is relative to the output (cancel or submit).
[_widgetVerticalPlacementConstraint setActive:NO];
if (item) {
_widgetVerticalPlacementConstraint =
[_shareView.bottomAnchor constraintEqualToAnchor:self.view.topAnchor];
} else {
_widgetVerticalPlacementConstraint =
[_shareView.topAnchor constraintEqualToAnchor:self.view.bottomAnchor];
}
[_widgetVerticalPlacementConstraint setActive:YES];
__weak ShareViewController* weakSelf = self;
[UIView animateWithDuration:ui_util::kAnimationDuration
animations:^{
[weakSelf.maskView setAlpha:0];
[weakSelf.view layoutIfNeeded];
}
completion:^(BOOL finished) {
NSArray* returnItem = item ? @[ item ] : @[];
if (error) {
[weakSelf.extensionContext cancelRequestWithError:error];
} else {
[weakSelf.extensionContext completeRequestReturningItems:returnItem
completionHandler:nil];
}
}];
}
- (void)queueActionItemURL:(NSURL*)URL
title:(NSString*)title
action:(app_group::ShareExtensionItemType)actionType
cancel:(BOOL)cancel
completion:(ProceduralBlock)completion {
NSURL* readingListURL = app_group::ExternalCommandsItemsFolder();
if (![[NSFileManager defaultManager]
fileExistsAtPath:[readingListURL path]]) {
[[NSFileManager defaultManager] createDirectoryAtPath:[readingListURL path]
withIntermediateDirectories:YES
attributes:nil
error:nil];
}
NSDate* date = [NSDate date];
NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
// This format sorts files by alphabetical order.
[dateFormatter setDateFormat:@"yyyy-MM-dd-HH-mm-ss.SSSSSS"];
NSTimeZone* timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
[dateFormatter setTimeZone:timeZone];
NSString* dateString = [dateFormatter stringFromDate:date];
NSURL* fileURL =
[readingListURL URLByAppendingPathComponent:dateString isDirectory:NO];
NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
if (URL)
[dict setObject:URL forKey:app_group::kShareItemURL];
if (title)
[dict setObject:title forKey:app_group::kShareItemTitle];
[dict setObject:date forKey:app_group::kShareItemDate];
[dict setObject:app_group::kShareItemSourceShareExtension
forKey:app_group::kShareItemSource];
if (!cancel) {
NSNumber* entryType = [NSNumber numberWithInteger:actionType];
[dict setObject:entryType forKey:app_group::kShareItemType];
}
[dict setValue:[NSNumber numberWithBool:cancel]
forKey:app_group::kShareItemCancel];
NSError* error = nil;
NSData* data = [NSKeyedArchiver archivedDataWithRootObject:dict
requiringSecureCoding:NO
error:&error];
if (!data || error) {
DLOG(WARNING) << "Error serializing data for title: "
<< base::SysNSStringToUTF8(title)
<< base::SysNSStringToUTF8([error description]);
return;
}
[[NSFileManager defaultManager] createFileAtPath:[fileURL path]
contents:data
attributes:nil];
if (completion) {
completion();
}
}
#pragma mark - ShareExtensionViewActionTarget
- (void)shareExtensionViewDidSelectCancel:(id)sender {
__weak ShareViewController* weakSelf = self;
[self
queueActionItemURL:nil
title:nil
action:app_group::READING_LIST_ITEM // Ignored
cancel:YES
completion:^{
[weakSelf
dismissAndReturnItem:nil
error:
[NSError
errorWithDomain:NSCocoaErrorDomain
code:NSUserCancelledError
userInfo:nil]];
}];
}
- (void)shareExtensionViewDidSelectAddToReadingList:(id)sender {
__weak ShareViewController* weakSelf = self;
[self queueActionItemURL:_shareURL
title:_shareTitle
action:app_group::READING_LIST_ITEM
cancel:NO
completion:^{
[weakSelf dismissAndReturnItem:weakSelf.shareItem error:nil];
}];
}
- (void)shareExtensionViewDidSelectAddToBookmarks:(id)sender {
__weak ShareViewController* weakSelf = self;
[self queueActionItemURL:_shareURL
title:_shareTitle
action:app_group::BOOKMARK_ITEM
cancel:NO
completion:^{
[weakSelf dismissAndReturnItem:weakSelf.shareItem error:nil];
}];
}
- (void)shareExtensionViewDidSelectOpenInChrome:(id)sender {
__weak ShareViewController* weakSelf = self;
AppGroupCommand* command = [[AppGroupCommand alloc]
initWithSourceApp:app_group::kOpenCommandSourceShareExtension
URLOpenerBlock:^(NSURL* openURL) {
ExtensionOpenURL(openURL, weakSelf, nil);
}];
[command prepareToOpenURL:_shareURL];
[command executeInApp];
[self queueActionItemURL:_shareURL
title:_shareTitle
action:app_group::OPEN_IN_CHROME_ITEM
cancel:NO
completion:^{
[weakSelf dismissAndReturnItem:weakSelf.shareItem error:nil];
}];
}
- (void)shareExtensionView:(id)sender
typeChanged:(app_group::ShareExtensionItemType)type {
[self setItemType:type];
}
@end