blob: 35c8ca367a32e1b02512ddbdce04df3927f096e2 [file] [log] [blame]
// Copyright 2016 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 "ios/chrome/browser/share_extension/share_extension_item_receiver.h"
#import <UIKit/UIKit.h>
#include "base/ios/block_types.h"
#include "base/mac/bind_objc_block.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_nsobject.h"
#include "base/metrics/histogram.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "ios/chrome/browser/experimental_flags.h"
#include "ios/chrome/browser/reading_list/reading_list_model.h"
#include "ios/chrome/browser/reading_list/reading_list_model_observer.h"
#include "ios/chrome/common/app_group/app_group_constants.h"
#include "ios/web/public/web_thread.h"
#import "net/base/mac/url_conversions.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Enum used to send metrics on item reception.
// If you change this enum, update histograms.xml.
enum ShareExtensionItemReceived {
INVALID_ENTRY = 0,
CANCELLED_ENTRY,
READINGLIST_ENTRY,
BOOKMARK_ENTRY,
SHARE_EXTENSION_ITEM_RECEIVED_COUNT
};
void LogHistogramReceivedItem(ShareExtensionItemReceived type) {
UMA_HISTOGRAM_ENUMERATION("IOS.ShareExtension.ReceivedEntry", type,
SHARE_EXTENSION_ITEM_RECEIVED_COUNT);
}
} // namespace
@interface ShareExtensionItemReceiver ()<NSFilePresenter> {
BOOL _isObservingFolder;
BOOL _folderCreated;
ReadingListModel* _readingListModel; // Not owned.
bookmarks::BookmarkModel* _bookmarkModel; // Not owned.
}
// Checks if the reading list folder is already created and if not, create it.
- (void)createReadingListFolder;
// Processes the data sent by the share extension. Data should be a NSDictionary
// serialized by +|NSKeyedArchiver archivedDataWithRootObject:|.
// |completion| is called if |data| has been fully processed.
- (BOOL)receivedData:(NSData*)data withCompletion:(ProceduralBlock)completion;
// Reads the file pointed by |url| and calls |receivedData:| on the content.
// If the file is processed, delete it.
// Must be called on the FILE thread.
// |completion| is only called if the file handling is completed without error.
- (void)handleFileAtURL:(NSURL*)url withCompletion:(ProceduralBlock)completion;
// Deletes the file pointed by |url| then call |completion|.
- (void)deleteFileAtURL:(NSURL*)url withCompletion:(ProceduralBlock)completion;
// Called on UIApplicationDidBecomeActiveNotification notification. Processes
// files that are already in the folder and starts observing the
// app_group::ShareExtensionItemsFolder() folder for new files.
- (void)applicationDidBecomeActive;
// Called on UIApplicationWillResignActiveNotification. Stops observing the
// app_group::ShareExtensionItemsFolder() folder for new files.
- (void)applicationWillResignActive;
// Called whenever a file is modified in app_group::ShareExtensionItemsFolder().
- (void)presentedSubitemDidChangeAtURL:(NSURL*)url;
@end
@implementation ShareExtensionItemReceiver
+ (ShareExtensionItemReceiver*)sharedInstance {
DCHECK_CURRENTLY_ON(web::WebThread::UI);
static ShareExtensionItemReceiver* instance =
[[ShareExtensionItemReceiver alloc] init];
return instance;
}
- (void)setBookmarkModel:(bookmarks::BookmarkModel*)bookmarkModel
readingListModel:(ReadingListModel*)readingListModel {
DCHECK_CURRENTLY_ON(web::WebThread::UI);
DCHECK(!_readingListModel);
DCHECK(!_bookmarkModel);
#if TARGET_IPHONE_SIMULATOR
if (![self presentedItemURL]) {
return;
}
#else
DCHECK([self presentedItemURL]);
#endif
_readingListModel = readingListModel;
_bookmarkModel = bookmarkModel;
web::WebThread::PostTask(web::WebThread::FILE, FROM_HERE,
base::BindBlock(^() {
[self createReadingListFolder];
}));
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(applicationDidBecomeActive)
name:UIApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(applicationWillResignActive)
name:UIApplicationWillResignActiveNotification
object:nil];
}
- (void)shutdown {
DCHECK_CURRENTLY_ON(web::WebThread::UI);
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (_isObservingFolder) {
[NSFileCoordinator removeFilePresenter:self];
}
_readingListModel = nil;
}
- (void)dealloc {
NOTREACHED();
}
- (void)createReadingListFolder {
DCHECK_CURRENTLY_ON(web::WebThread::FILE);
if (![[NSFileManager defaultManager]
fileExistsAtPath:[[self presentedItemURL] path]]) {
[[NSFileManager defaultManager]
createDirectoryAtPath:[[self presentedItemURL] path]
withIntermediateDirectories:NO
attributes:nil
error:nil];
}
web::WebThread::PostTask(
web::WebThread::UI, FROM_HERE, base::BindBlock(^() {
if ([[UIApplication sharedApplication] applicationState] ==
UIApplicationStateActive) {
_folderCreated = YES;
[self applicationDidBecomeActive];
}
}));
}
- (BOOL)receivedData:(NSData*)data withCompletion:(ProceduralBlock)completion {
id entryID = [NSKeyedUnarchiver unarchiveObjectWithData:data];
NSDictionary* entry = base::mac::ObjCCast<NSDictionary>(entryID);
if (!entry) {
if (completion) {
completion();
}
return NO;
}
NSNumber* cancelled = base::mac::ObjCCast<NSNumber>(
[entry objectForKey:app_group::kShareItemCancel]);
if (!cancelled) {
if (completion) {
completion();
}
return NO;
}
if ([cancelled boolValue]) {
LogHistogramReceivedItem(CANCELLED_ENTRY);
if (completion) {
completion();
}
return YES;
}
GURL entryURL =
net::GURLWithNSURL([entry objectForKey:app_group::kShareItemURL]);
std::string entryTitle =
base::SysNSStringToUTF8([entry objectForKey:app_group::kShareItemTitle]);
NSDate* entryDate = base::mac::ObjCCast<NSDate>(
[entry objectForKey:app_group::kShareItemDate]);
NSNumber* entryType = base::mac::ObjCCast<NSNumber>(
[entry objectForKey:app_group::kShareItemType]);
if (!entryURL.is_valid() || !entryDate || !entryType) {
if (completion) {
completion();
}
return NO;
}
UMA_HISTOGRAM_TIMES("IOS.ShareExtension.ReceivedEntryDelay",
base::TimeDelta::FromSecondsD(
[[NSDate date] timeIntervalSinceDate:entryDate]));
// Entry is valid. Add it to the reading list model.
web::WebThread::PostTask(web::WebThread::UI, FROM_HERE, base::BindBlock(^() {
if (!_readingListModel || !_bookmarkModel) {
// Models may have been deleted after the file
// processing started.
return;
}
app_group::ShareExtensionItemType type =
static_cast<app_group::ShareExtensionItemType>(
[entryType integerValue]);
if (type == app_group::READING_LIST_ITEM) {
LogHistogramReceivedItem(READINGLIST_ENTRY);
_readingListModel->AddEntry(entryURL,
entryTitle);
}
if (type == app_group::BOOKMARK_ITEM) {
LogHistogramReceivedItem(BOOKMARK_ENTRY);
_bookmarkModel->AddURL(
_bookmarkModel->mobile_node(), 0,
base::ASCIIToUTF16(entryTitle), entryURL);
}
if (completion) {
web::WebThread::PostTask(web::WebThread::FILE,
FROM_HERE,
base::BindBlock(^() {
completion();
}));
}
}));
return YES;
}
- (void)handleFileAtURL:(NSURL*)url withCompletion:(ProceduralBlock)completion {
DCHECK_CURRENTLY_ON(web::WebThread::FILE);
if (![[NSFileManager defaultManager] fileExistsAtPath:[url path]]) {
// The handler is called on file modification, including deletion. Check
// that the file exists before continuing.
return;
}
ProceduralBlock successCompletion = ^{
DCHECK_CURRENTLY_ON(web::WebThread::FILE);
[self deleteFileAtURL:url withCompletion:completion];
};
NSError* error = nil;
base::scoped_nsobject<NSFileCoordinator> readingCoordinator(
[[NSFileCoordinator alloc] initWithFilePresenter:self]);
[readingCoordinator
coordinateReadingItemAtURL:url
options:NSFileCoordinatorReadingWithoutChanges
error:&error
byAccessor:^(NSURL* newURL) {
NSData* data = [[NSFileManager defaultManager]
contentsAtPath:[newURL path]];
if (![self receivedData:data
withCompletion:successCompletion]) {
LogHistogramReceivedItem(INVALID_ENTRY);
}
}];
}
- (void)deleteFileAtURL:(NSURL*)url withCompletion:(ProceduralBlock)completion {
DCHECK_CURRENTLY_ON(web::WebThread::FILE);
base::scoped_nsobject<NSFileCoordinator> deletingCoordinator(
[[NSFileCoordinator alloc] initWithFilePresenter:self]);
NSError* error = nil;
[deletingCoordinator
coordinateWritingItemAtURL:url
options:NSFileCoordinatorWritingForDeleting
error:&error
byAccessor:^(NSURL* newURL) {
[[NSFileManager defaultManager] removeItemAtURL:newURL
error:nil];
}];
if (completion) {
completion();
}
}
- (void)applicationDidBecomeActive {
if (!_folderCreated || _isObservingFolder) {
return;
}
_isObservingFolder = YES;
// Start observing for new files.
[NSFileCoordinator addFilePresenter:self];
// There may already be files. Process them.
web::WebThread::PostTask(
web::WebThread::FILE, FROM_HERE, base::BindBlock(^() {
NSArray<NSURL*>* files = [[NSFileManager defaultManager]
contentsOfDirectoryAtURL:[self presentedItemURL]
includingPropertiesForKeys:nil
options:NSDirectoryEnumerationSkipsHiddenFiles
error:nil];
if ([files count] == 0) {
return;
}
web::WebThread::PostTask(
web::WebThread::UI, FROM_HERE, base::BindBlock(^() {
UMA_HISTOGRAM_COUNTS_100(
"IOS.ShareExtension.ReceivedEntriesCount", [files count]);
for (NSURL* fileURL : files) {
__block std::unique_ptr<
ReadingListModel::ScopedReadingListBatchUpdate>
batchToken(_readingListModel->BeginBatchUpdates());
web::WebThread::PostTask(
web::WebThread::FILE, FROM_HERE, base::BindBlock(^() {
[self handleFileAtURL:fileURL
withCompletion:^{
web::WebThread::PostTask(web::WebThread::UI,
FROM_HERE,
base::BindBlock(^() {
batchToken.reset();
}));
}];
}));
}
}));
}));
}
- (void)applicationWillResignActive {
if (!_isObservingFolder) {
return;
}
_isObservingFolder = NO;
[NSFileCoordinator removeFilePresenter:self];
}
#pragma mark -
#pragma mark NSFilePresenter methods
- (void)presentedSubitemDidChangeAtURL:(NSURL*)url {
web::WebThread::PostTask(web::WebThread::FILE, FROM_HERE,
base::BindBlock(^() {
[self handleFileAtURL:url withCompletion:nil];
}));
}
- (NSOperationQueue*)presentedItemOperationQueue {
return [NSOperationQueue mainQueue];
}
- (NSURL*)presentedItemURL {
return app_group::ShareExtensionItemsFolder();
}
@end