blob: 53c05293e360758ff4a5a26ce320b2a41a0a3713 [file] [log] [blame]
// Copyright 2025 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/share_extension/model/share_extension_controller.h"
#import <UIKit/UIKit.h>
#import "base/apple/foundation_util.h"
#import "base/functional/bind.h"
#import "base/ios/block_types.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_macros.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/task/bind_post_task.h"
#import "base/task/sequenced_task_runner.h"
#import "base/task/thread_pool.h"
#import "base/threading/scoped_blocking_call.h"
#import "google_apis/gaia/gaia_id.h"
#import "ios/chrome/browser/share_extension/model/bookmark_adder.h"
#import "ios/chrome/browser/share_extension/model/parsed_share_extension_entry.h"
#import "ios/chrome/browser/share_extension/model/reading_list_adder.h"
#import "ios/chrome/browser/share_extension/model/share_extension_utils.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/profile/features.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/signin/model/account_profile_mapper.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
#import "net/base/apple/url_conversions.h"
#import "url/gurl.h"
namespace {
// Enum used to send metrics on item reception.
// If you change this enum, update histograms.xml.
enum ShareExtensionSource {
UNKNOWN_SOURCE = 0,
SHARE_EXTENSION,
SHARE_EXTENSION_SOURCE_COUNT
};
enum ShareExtensionItemReceived {
INVALID_ENTRY = 0,
CANCELLED_ENTRY,
READINGLIST_ENTRY,
BOOKMARK_ENTRY,
OPEN_IN_CHROME_ENTRY,
OPEN_IN_CHROME_INCOGNITO_ENTRY,
IMAGE_SEARCH_ENTRY,
TEXT_SEARCH_ENTRY,
INCOGNITO_IMAGE_SEARCH_ENTRY,
INCOGNITO_TEXT_SEARCH_ENTRY,
SHARE_EXTENSION_ITEM_RECEIVED_COUNT
};
void DeleteFileAtUrl(NSURL* url) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
[[NSFileManager defaultManager] removeItemAtURL:url error:nil];
}
void LogHistogramReceivedItem(ShareExtensionItemReceived type) {
UMA_HISTOGRAM_ENUMERATION("IOS.ShareExtension.ReceivedEntry", type,
SHARE_EXTENSION_ITEM_RECEIVED_COUNT);
}
ShareExtensionSource SourceIDFromSource(NSString* source) {
if ([source isEqualToString:app_group::kShareItemSourceShareExtension]) {
return SHARE_EXTENSION;
}
return UNKNOWN_SOURCE;
}
template <typename T>
void OnProfileLoaded(std::unique_ptr<T> adder,
ScopedProfileKeepAliveIOS keep_alive) {
T* adder_ptr = adder.get();
adder_ptr->OnProfileLoaded(
std::move(keep_alive),
base::BindOnce([](std::unique_ptr<T> adder) {}, std::move(adder)));
}
template <typename Adder, typename... Args>
void AddDataToProfileByGaiaID(NSString* gaiaID, Args&&... args) {
std::optional<std::string> profileName =
GetApplicationContext()
->GetAccountProfileMapper()
->FindProfileNameForGaiaID(GaiaId(gaiaID));
if (profileName.has_value()) {
ProfileManagerIOS* profileManager =
GetApplicationContext()->GetProfileManager();
auto adder = std::make_unique<Adder>(std::forward<Args>(args)...);
profileManager->LoadProfileAsync(
*profileName,
base::BindOnce(&OnProfileLoaded<Adder>, std::move(adder)));
}
}
} // namespace
@interface ShareExtensionController () <NSFilePresenter>
@end
@implementation ShareExtensionController {
BOOL _isObservingReadingListFolder;
BOOL _readingListFolderCreated;
BOOL _shutdownCalled;
scoped_refptr<base::SequencedTaskRunner> _taskRunner;
SEQUENCE_CHECKER(_sequenceChecker);
}
#pragma mark - NSObject lifetime
- (void)dealloc {
DCHECK(!_taskRunner) << "-shutdown must be called before -dealloc";
}
#pragma mark - Public
- (instancetype)init {
self = [super init];
if (![self presentedItemURL]) {
return nil;
}
if (self) {
_taskRunner = base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT});
}
return self;
}
- (void)shutdown {
_shutdownCalled = YES;
if (_isObservingReadingListFolder) {
[NSFileCoordinator removeFilePresenter:self];
}
_taskRunner = nullptr;
}
- (void)startFilesProcessing {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (_shutdownCalled) {
return;
}
NSURL* filesFolderURL = [self presentedItemURL];
if (!filesFolderURL) {
return;
}
__weak ShareExtensionController* weakSelf = self;
_taskRunner->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&CreateShareExtensionFilesDirectory, filesFolderURL),
base::BindOnce(^(bool success) {
[weakSelf handleReadingListFolderCreationWithSuccess:success];
}));
}
#pragma mark - NSFilePresenter methods
- (void)presentedSubitemDidChangeAtURL:(NSURL*)url {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (_shutdownCalled) {
return;
}
[self handleFileAtURL:url];
}
- (NSOperationQueue*)presentedItemOperationQueue {
return [NSOperationQueue mainQueue];
}
- (NSURL*)presentedItemURL {
return app_group::ExternalCommandsItemsFolder();
}
#pragma mark - Private
- (void)startObservingReadingListFolder {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
CHECK(!_shutdownCalled);
if (!_isObservingReadingListFolder) {
// Process the existing files first then start observing the folder for any
// new added file.
[self processExistingFiles];
_isObservingReadingListFolder = YES;
[NSFileCoordinator addFilePresenter:self];
}
}
- (void)stopObservingReadingListFolder {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (_isObservingReadingListFolder) {
_isObservingReadingListFolder = NO;
[NSFileCoordinator removeFilePresenter:self];
}
}
- (void)handleReadingListFolderCreationWithSuccess:(BOOL)success {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (_shutdownCalled) {
return;
}
if (success) {
[self readingListFolderCreated];
}
}
- (void)readingListFolderCreated {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (_shutdownCalled) {
return;
}
_readingListFolderCreated = YES;
[self startObservingReadingListFolder];
}
- (void)applicationDidBecomeActive {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
CHECK(!_shutdownCalled);
if (!_readingListFolderCreated) {
return;
}
if (!_isObservingReadingListFolder) {
[self startObservingReadingListFolder];
}
}
- (void)applicationWillResignActive {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
CHECK(!_shutdownCalled);
[self stopObservingReadingListFolder];
}
- (void)processExistingFiles {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (_shutdownCalled) {
return;
}
NSURL* filesDirectory = [self presentedItemURL];
if (!filesDirectory) {
return;
}
__weak ShareExtensionController* weakSelf = self;
_taskRunner->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&EnumerateFilesInDirectory, filesDirectory),
base::BindOnce(^(NSArray<NSURL*>* files) {
[weakSelf handleDirectoryEnumerationResult:files];
}));
}
- (void)handleDirectoryEnumerationResult:(NSArray<NSURL*>*)files {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (_shutdownCalled) {
return;
}
const NSUInteger filesCount = [files count];
if (filesCount) {
UMA_HISTOGRAM_COUNTS_100("IOS.ShareExtension.ReceivedEntriesCount",
filesCount);
for (NSURL* fileURL : files) {
[self handleFileAtURL:fileURL];
}
}
}
- (void)handleFileAtURL:(NSURL*)url {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
CHECK(!_shutdownCalled);
__weak ShareExtensionController* weakSelf = self;
ProceduralBlock successCompletion = base::CallbackToBlock(
base::BindPostTask(_taskRunner, base::BindOnce(&DeleteFileAtUrl, url)));
_taskRunner->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&PerformBlockingFileReadAndParse, url),
base::BindOnce(^(ParsedShareExtensionEntry* parsedEntry) {
[weakSelf handleParsedShareEntryResult:parsedEntry
originalFileURL:url
completion:successCompletion];
}));
}
- (void)handleParsedShareEntryResult:(ParsedShareExtensionEntry*)parsedEntry
originalFileURL:(NSURL*)originalFileURL
completion:(ProceduralBlock)completion {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (_shutdownCalled) {
return;
}
if (![parsedEntry parsedEntryIsValid]) {
LogHistogramReceivedItem(INVALID_ENTRY);
if (completion) {
completion();
}
return;
}
if (parsedEntry.cancelled) {
LogHistogramReceivedItem(CANCELLED_ENTRY);
if (completion) {
completion();
}
return;
}
UMA_HISTOGRAM_TIMES(
"IOS.ShareExtension.ReceivedEntryDelay",
base::Seconds([[NSDate date] timeIntervalSinceDate:parsedEntry.date]));
UMA_HISTOGRAM_ENUMERATION("IOS.ShareExtension.Source",
SourceIDFromSource(parsedEntry.source),
SHARE_EXTENSION_SOURCE_COUNT);
[self processEntryWithType:parsedEntry.type
title:parsedEntry.title
gaiaID:parsedEntry.gaiaID
URL:parsedEntry.url
completion:completion];
}
- (void)processEntryWithType:(app_group::ShareExtensionItemType)entryType
title:(NSString*)entryTitle
gaiaID:(NSString*)gaiaID
URL:(NSURL*)entryURL
completion:(ProceduralBlock)completion {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
switch (entryType) {
case app_group::BOOKMARK_ITEM: {
LogHistogramReceivedItem(BOOKMARK_ENTRY);
[self addBookmarkToProfileByGaiaID:gaiaID
URL:entryURL
bookmarkTitle:entryTitle];
break;
}
case app_group::READING_LIST_ITEM: {
LogHistogramReceivedItem(READINGLIST_ENTRY);
[self addReadingListToProfileByGaiaID:gaiaID
URL:entryURL
readingListTitle:entryTitle];
break;
}
case app_group::OPEN_IN_CHROME_ITEM: {
LogHistogramReceivedItem(OPEN_IN_CHROME_ENTRY);
break;
}
case app_group::OPEN_IN_CHROME_INCOGNITO_ITEM: {
LogHistogramReceivedItem(OPEN_IN_CHROME_INCOGNITO_ENTRY);
break;
}
case app_group::IMAGE_SEARCH_ITEM: {
LogHistogramReceivedItem(IMAGE_SEARCH_ENTRY);
break;
}
case app_group::TEXT_SEARCH_ITEM: {
LogHistogramReceivedItem(TEXT_SEARCH_ENTRY);
break;
}
case app_group::INCOGNITO_IMAGE_SEARCH_ITEM: {
LogHistogramReceivedItem(INCOGNITO_IMAGE_SEARCH_ENTRY);
break;
}
case app_group::INCOGNITO_TEXT_SEARCH_ITEM: {
LogHistogramReceivedItem(INCOGNITO_TEXT_SEARCH_ENTRY);
break;
}
}
if (completion) {
completion();
}
}
- (void)addBookmarkToProfileByGaiaID:(NSString*)gaiaID
URL:(NSURL*)URL
bookmarkTitle:(NSString*)bookmarkTitle {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
AddDataToProfileByGaiaID<BookmarkAdder>(
gaiaID, net::GURLWithNSURL(URL), base::SysNSStringToUTF8(bookmarkTitle));
// std::optional<std::string> profileName =
// GetApplicationContext()
// ->GetAccountProfileMapper()
// ->FindProfileNameForGaiaID(GaiaId(gaiaID));
//
// if (profileName.has_value()) {
// std::string title = base::SysNSStringToUTF8(bookmarkTitle);
// ProfileManagerIOS* profileManager =
// GetApplicationContext()->GetProfileManager();
// std::unique_ptr<BookmarkAdder> adder = std::make_unique<BookmarkAdder>(
// net::GURLWithNSURL(URL), std::move(title));
// profileManager->LoadProfileAsync(
// *profileName,
// base::BindOnce(&OnProfileLoaded<BookmarkAdder>, std::move(adder)));
// }
}
- (void)addReadingListToProfileByGaiaID:(NSString*)gaiaID
URL:(NSURL*)URL
readingListTitle:(NSString*)readingListTitle {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
AddDataToProfileByGaiaID<ReadingListAdder>(
gaiaID, net::GURLWithNSURL(URL),
base::SysNSStringToUTF8(readingListTitle));
// std::optional<std::string> profileName =
// GetApplicationContext()
// ->GetAccountProfileMapper()
// ->FindProfileNameForGaiaID(GaiaId(gaiaID));
//
// if (profileName.has_value()) {
// std::string title = base::SysNSStringToUTF8(readingListTitle);
// ProfileManagerIOS* profileManager =
// GetApplicationContext()->GetProfileManager();
// std::unique_ptr<ReadingListAdder> adder =
// std::make_unique<ReadingListAdder>(net::GURLWithNSURL(URL), title,
// profileManager);
// profileManager->LoadProfileAsync(
// *profileName,
// base::BindOnce(&OnProfileLoaded<ReadingListAdder>,
// std::move(adder)));
// }
}
@end