// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "image_copier.h"
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import "base/functional/bind.h"
#import "base/metrics/histogram_macros.h"
#import "base/strings/sys_string_conversions.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/shared/ui/util/pasteboard_util.h"
#import "ios/chrome/browser/web/model/image_fetch/image_fetch_tab_helper.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
#import "ui/base/l10n/l10n_util.h"
namespace {
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// Enum for the Mobile.ContextMenu.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;
} // namespace
@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)stop {
self.browser = nullptr;
[self stopAlertCoordinator];
}
- (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 stopAlertCoordinator];
weakSelf.activeID = kNoActiveCopy;
ImageCopyResult result =
StoreImageInPasteboard(data, [NSURL URLWithString:urlStr]);
switch (result) {
case ImageCopyResult::kImage:
[weakSelf recordCopyImageUMA:ContextMenuCopyImage::kImageCopied];
break;
case ImageCopyResult::kURL:
[weakSelf recordCopyImageUMA:ContextMenuCopyImage::kURLCopied];
break;
}
[weakSelf recordCopyImageUMA:ContextMenuCopyImage::kTryCopyImage];
}
[weakSelf recordCopyImageUMA:ContextMenuCopyImage::kImageFetched];
});
// Dismiss current alert.
[self stopAlertCoordinator];
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 stopAlertCoordinator];
[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];
}
#pragma mark - Private
// Records in UMA the Copy Image with `UMAEnum`.
- (void)recordCopyImageUMA:(ContextMenuCopyImage)UMAEnum {
UMA_HISTOGRAM_ENUMERATION("Mobile.ContextMenu.CopyImage", UMAEnum);
}
// Stops the alert coordinator.
- (void)stopAlertCoordinator {
[self.alertCoordinator stop];
self.alertCoordinator = nil;
}
@end