blob: 0c69aaf18f030def800c9225934b917b38d85768 [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 "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/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,
// The URL of the image is copied.
kURLCopied = 6,
kMaxValue = kURLCopied,
};
// 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 ()
// 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, assign) 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, assign) int activeID;
@end
@implementation ImageCopier
- (instancetype)initWithBrowser:(Browser*)browser {
self = [super init];
if (self) {
_idGenerator = 1;
_activeID = kNoActiveCopy;
_browser = browser;
}
return self;
}
- (void)copyImageAtURL:(const GURL&)url
referrer:(const web::Referrer&)referrer
webState:(web::WebState*)webState
baseViewController:(UIViewController*)baseViewController {
__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 data to pasteboard. Don't copy the URL otherwise some apps
// will paste the text and not the image. See crbug.com/1270239.
NSMutableDictionary* item =
[NSMutableDictionary dictionaryWithCapacity:1];
NSString* uti = GetImageUTIFromData(data);
if (uti) {
[item setValue:data forKey:uti];
[weakSelf recordCopyImageUMA:ContextMenuCopyImage::kImageCopied];
} else {
[item setValue:[NSURL URLWithString:urlStr]
forKey:(__bridge NSString*)kUTTypeURL];
[weakSelf recordCopyImageUMA:ContextMenuCopyImage::kURLCopied];
}
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: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`.
web::GetUIThreadTaskRunner({})->PostDelayedTask(
FROM_HERE, base::BindOnce(^{
// Checks that the copy has not finished yet.
if (callbackID == weakSelf.activeID) {
[weakSelf.alertCoordinator start];
[weakSelf recordCopyImageUMA:ContextMenuCopyImage::kAlertPopUp];
}
}),
base::Milliseconds(kAlertDelayInMs));
[self recordCopyImageUMA:ContextMenuCopyImage::kInvoked];
}
- (void)recordCopyImageUMA:(ContextMenuCopyImage)UMAEnum {
UMA_HISTOGRAM_ENUMERATION(kUmaContextMenuCopyImage, UMAEnum);
}
@end