blob: d9371e81d9e9d79e5c42dab608cb635b35019932 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "image_copier.h"
#include <MobileCoreServices/MobileCoreServices.h>
#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#import "base/strings/sys_string_conversions.h"
#include "base/task/post_task.h"
#include "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/main/browser.h"
#import "ios/chrome/browser/ui/alert_coordinator/alert_coordinator.h"
#import "ios/chrome/browser/ui/image_util/image_util.h"
#import "ios/chrome/browser/web/image_fetch_tab_helper.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ios/web/public/thread/web_task_traits.h"
#include "ios/web/public/thread/web_thread.h"
#include "ui/base/l10n/l10n_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Name of the UMA ContextMenu.iOS.CopyImage histogram.
const char kUmaContextMenuCopyImage[] = "ContextMenu.iOS.CopyImage";
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// Enum for the ContextMenu.iOS.CopyImage UMA histogram to report
// the results of Copy Image.
enum class ContextMenuCopyImage {
// Copy Image is called.
kInvoked = 0,
// Image data is fetched.
kImageFetched = 1,
// Image data is fetched, and Copy Image is not canceled by user.
kTryCopyImage = 2,
// Fetched image data is valid and copied to pasteboard.
kImageCopied = 3,
// Fetching image data takes too long, a waiting alert popped up.
kAlertPopUp = 4,
// Copy Image is canceled by user from the alert.
kCanceled = 5,
kMaxValue = kCanceled,
};
// Time Period between "Copy Image" is clicked and "Copying..." alert is
// launched.
const int kAlertDelayInMs = 300;
// A speical id indicates that last copy is finished or canceled and next
// copy has not started.
const int kNoActiveCopy = 0;
}
@interface ImageCopier ()
// Base view controller for the alerts.
@property(nonatomic, weak) UIViewController* baseViewController;
// The browser.
@property(nonatomic, assign) Browser* browser;
// Alert coordinator to give feedback to the user.
@property(nonatomic, strong) AlertCoordinator* alertCoordinator;
// A counter which generates one ID for each call on
// CopyImageAtURL:referrer:webState.
@property(nonatomic) int idGenerator;
// ID of current active copy. A copy is active after
// CopyImageAtURL:referrer:webState is called, and before user cancels the
// copy or the copy finishes.
@property(nonatomic) int activeID;
@end
@implementation ImageCopier
@synthesize alertCoordinator = _alertCoordinator;
@synthesize baseViewController = _baseViewController;
@synthesize idGenerator = _idGenerator;
@synthesize activeID = _activeID;
@synthesize browser = _browser;
- (instancetype)initWithBaseViewController:(UIViewController*)baseViewController
browser:(Browser*)browser {
self = [super init];
if (self) {
self.idGenerator = 1;
self.activeID = kNoActiveCopy;
self.baseViewController = baseViewController;
self.browser = browser;
}
return self;
}
- (void)copyImageAtURL:(const GURL&)url
referrer:(const web::Referrer&)referrer
webState:(web::WebState*)webState {
__weak ImageCopier* weakSelf = self;
// |idGenerator| is initiated to 1 and incremented by 2, so it will always be
// odd number and won't collides with |kNoActiveCopy| or nil.activeID, which
// are 0s.
self.idGenerator += 2;
self.activeID = self.idGenerator;
// This var is to be captured by blocks in ImageFetchTabHelper::GetImageData
// and web::WebThread::PostDelayedTask. When a block is invoked, it uses the
// captured |callbackID| to check if the copy from where it's started is still
// alive or has been finished/canceled.
int callbackID = self.idGenerator;
ImageFetchTabHelper* tabHelper = ImageFetchTabHelper::FromWebState(webState);
DCHECK(tabHelper);
NSString* urlStr = base::SysUTF8ToNSString(url.spec());
tabHelper->GetImageData(url, referrer, ^(NSData* data) {
// Check that the copy has not been canceled.
if (callbackID == weakSelf.activeID) {
[weakSelf.alertCoordinator stop];
weakSelf.activeID = kNoActiveCopy;
// Copy image url and data to pasteboard.
NSMutableDictionary* item =
[NSMutableDictionary dictionaryWithCapacity:3];
[item setValue:urlStr forKey:(__bridge NSString*)kUTTypeText];
[item setValue:[NSURL URLWithString:urlStr]
forKey:(__bridge NSString*)kUTTypeURL];
NSString* uti = GetImageUTIFromData(data);
if (uti) {
[item setValue:data forKey:uti];
[weakSelf recordCopyImageUMA:ContextMenuCopyImage::kImageCopied];
}
UIPasteboard.generalPasteboard.items =
[NSMutableArray arrayWithObject:item];
[weakSelf recordCopyImageUMA:ContextMenuCopyImage::kTryCopyImage];
}
[weakSelf recordCopyImageUMA:ContextMenuCopyImage::kImageFetched];
});
// Dismiss current alert.
[self.alertCoordinator stop];
self.alertCoordinator = [[AlertCoordinator alloc]
initWithBaseViewController:self.baseViewController
browser:self.browser
title:l10n_util::GetNSStringWithFixup(
IDS_IOS_CONTENT_COPYIMAGE_ALERT_COPYING)
message:nil];
[self.alertCoordinator
addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CANCEL)
action:^() {
// Cancels current copy and closes the alert.
weakSelf.activeID = kNoActiveCopy;
[weakSelf.alertCoordinator stop];
[weakSelf recordCopyImageUMA:ContextMenuCopyImage::kCanceled];
}
style:UIAlertActionStyleCancel];
// Delays launching alert by |kAlertDelayInMs|.
base::PostDelayedTask(
FROM_HERE, {web::WebThread::UI}, base::BindOnce(^{
// Checks that the copy has not finished yet.
if (callbackID == weakSelf.activeID) {
[weakSelf.alertCoordinator start];
[weakSelf recordCopyImageUMA:ContextMenuCopyImage::kAlertPopUp];
}
}),
base::TimeDelta::FromMilliseconds(kAlertDelayInMs));
[self recordCopyImageUMA:ContextMenuCopyImage::kInvoked];
}
- (void)recordCopyImageUMA:(ContextMenuCopyImage)UMAEnum {
UMA_HISTOGRAM_ENUMERATION(kUmaContextMenuCopyImage, UMAEnum);
}
@end