[go: nahoru, domu]

blob: f8d2f145a4b8ec8187548dd88893bee0f5e4776c [file] [log] [blame]
// Copyright 2015 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/app/spotlight/bookmarks_spotlight_manager.h"
#import <memory>
#import <CoreSpotlight/CoreSpotlight.h>
#import "base/apple/foundation_util.h"
#import "base/metrics/histogram_macros.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "base/version.h"
#import "components/bookmarks/browser/base_bookmark_model_observer.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "ios/chrome/app/spotlight/searchable_item_factory.h"
#import "ios/chrome/app/spotlight/spotlight_interface.h"
#import "ios/chrome/app/spotlight/spotlight_logger.h"
#import "ios/chrome/browser/bookmarks/local_or_syncable_bookmark_model_factory.h"
#import "ios/chrome/browser/favicon/ios_chrome_large_icon_service_factory.h"
namespace {
// Limit the size of the initial indexing. This will not limit the size of the
// index as new bookmarks can be added afterwards.
const int kMaxInitialIndexSize = 1000;
// Minimum delay between two global indexing of bookmarks.
const base::TimeDelta kDelayBetweenTwoIndexing = base::Days(7);
} // namespace
class SpotlightBookmarkModelBridge;
// Called from the BrowserBookmarkModelBridge from C++ -> ObjC.
@interface BookmarksSpotlightManager ()
// Detaches the `SpotlightBookmarkModelBridge` from the bookmark model. The
// manager must not be used after calling this method.
- (void)detachBookmarkModel;
// Removes the node from the Spotlight index.
- (void)removeNodeFromIndex:(const bookmarks::BookmarkNode*)node;
// Refreshes all nodes in the subtree of node.
// If `initial` is YES, limit the number of nodes to kMaxInitialIndexSize.
- (void)refreshNodeInIndex:(const bookmarks::BookmarkNode*)node;
// Returns true is the current index is too old or from an incompatible version.
- (BOOL)shouldReindex;
// Clears all bookmark items in spotlight.
- (void)clearAllBookmarkSpotlightItems:(BlockWithError)completionHandler;
@end
// Handles notification that bookmarks has been removed changed so we can update
// the Spotlight index.
class SpotlightBookmarkModelBridge : public bookmarks::BookmarkModelObserver {
public:
explicit SpotlightBookmarkModelBridge(BookmarksSpotlightManager* owner)
: owner_(owner) {}
~SpotlightBookmarkModelBridge() override {}
void BookmarkNodeRemoved(bookmarks::BookmarkModel* model,
const bookmarks::BookmarkNode* parent,
size_t old_index,
const bookmarks::BookmarkNode* node,
const std::set<GURL>& removed_urls) override {}
void OnWillRemoveBookmarks(bookmarks::BookmarkModel* model,
const bookmarks::BookmarkNode* parent,
size_t old_index,
const bookmarks::BookmarkNode* node) override {
[owner_ removeNodeFromIndex:node];
}
void BookmarkModelBeingDeleted(bookmarks::BookmarkModel* model) override {
[owner_ detachBookmarkModel];
}
void BookmarkModelLoaded(bookmarks::BookmarkModel* model,
bool ids_reassigned) override {
[owner_ reindexBookmarksIfNeeded];
}
void BookmarkNodeAdded(bookmarks::BookmarkModel* model,
const bookmarks::BookmarkNode* parent,
size_t index,
bool added_by_user) override {
[owner_ refreshNodeInIndex:parent->children()[index].get()];
}
void OnWillChangeBookmarkNode(bookmarks::BookmarkModel* model,
const bookmarks::BookmarkNode* node) override {
[owner_ removeNodeFromIndex:node];
}
void BookmarkNodeChanged(bookmarks::BookmarkModel* model,
const bookmarks::BookmarkNode* node) override {
[owner_ refreshNodeInIndex:node];
}
void BookmarkNodeFaviconChanged(
bookmarks::BookmarkModel* model,
const bookmarks::BookmarkNode* node) override {
[owner_ refreshNodeInIndex:node];
}
void BookmarkAllUserNodesRemoved(
bookmarks::BookmarkModel* model,
const std::set<GURL>& removed_urls) override {
[owner_ clearAllBookmarkSpotlightItems:nil];
}
void BookmarkNodeChildrenReordered(
bookmarks::BookmarkModel* model,
const bookmarks::BookmarkNode* node) override {}
void BookmarkNodeMoved(bookmarks::BookmarkModel* model,
const bookmarks::BookmarkNode* old_parent,
size_t old_index,
const bookmarks::BookmarkNode* new_parent,
size_t new_index) override {
[owner_ refreshNodeInIndex:new_parent->children()[new_index].get()];
}
private:
__weak BookmarksSpotlightManager* owner_;
};
@implementation BookmarksSpotlightManager {
// Bridge to register for bookmark changes.
std::unique_ptr<SpotlightBookmarkModelBridge> _bookmarkModelBridge;
// Keep a reference to detach before deallocing. Life cycle of _bookmarkModel
// is longer than life cycle of a SpotlightManager as
// `BookmarkModelBeingDeleted` will cause deletion of SpotlightManager.
bookmarks::BookmarkModel* _bookmarkModel; // weak
// Number of nodes indexed in initial scan.
NSUInteger _nodesIndexed;
// Tracks whether initial indexing has been done.
BOOL _initialIndexDone;
}
+ (BookmarksSpotlightManager*)bookmarksSpotlightManagerWithBrowserState:
(ChromeBrowserState*)browserState {
favicon::LargeIconService* largeIconService =
IOSChromeLargeIconServiceFactory::GetForBrowserState(browserState);
return [[BookmarksSpotlightManager alloc]
initWithLargeIconService:largeIconService
bookmarkModel:ios::LocalOrSyncableBookmarkModelFactory::
GetForBrowserState(browserState)
spotlightInterface:[SpotlightInterface defaultInterface]
searchableItemFactory:
[[SearchableItemFactory alloc]
initWithLargeIconService:largeIconService
domain:spotlight::DOMAIN_BOOKMARKS
useTitleInIdentifiers:YES]];
}
- (instancetype)
initWithLargeIconService:(favicon::LargeIconService*)largeIconService
bookmarkModel:(bookmarks::BookmarkModel*)bookmarkModel
spotlightInterface:(SpotlightInterface*)spotlightInterface
searchableItemFactory:(SearchableItemFactory*)searchableItemFactory {
self = [super init];
if (self) {
_pendingLargeIconTasksCount = 0;
_searchableItemFactory = searchableItemFactory;
_spotlightInterface = spotlightInterface;
_bookmarkModelBridge.reset(new SpotlightBookmarkModelBridge(self));
_bookmarkModel = bookmarkModel;
bookmarkModel->AddObserver(_bookmarkModelBridge.get());
}
return self;
}
- (void)detachBookmarkModel {
if (_bookmarkModelBridge.get()) {
_bookmarkModel->RemoveObserver(_bookmarkModelBridge.get());
_bookmarkModelBridge.reset();
}
}
- (void)clearAllBookmarkSpotlightItems:(BlockWithError)completionHandler {
[self.searchableItemFactory cancelItemsGeneration];
[self.spotlightInterface
deleteSearchableItemsWithDomainIdentifiers:@[
spotlight::StringFromSpotlightDomain(spotlight::DOMAIN_BOOKMARKS)
]
completionHandler:completionHandler];
}
- (NSMutableArray*)parentFolderNamesForNode:
(const bookmarks::BookmarkNode*)node {
if (!node) {
return [[NSMutableArray alloc] init];
}
NSMutableArray* parentNames = [self parentFolderNamesForNode:node->parent()];
if (node->is_folder() && !_bookmarkModel->is_permanent_node(node)) {
[parentNames addObject:base::SysUTF16ToNSString(node->GetTitle())];
}
return parentNames;
}
- (void)removeNodeFromIndex:(const bookmarks::BookmarkNode*)node {
if (node->is_url()) {
[self removeURLNodeFromIndex:node];
return;
}
for (const auto& child : node->children())
[self removeNodeFromIndex:child.get()];
}
// Helper to remove URL nodes at the leaves of the bookmark index.
- (void)removeURLNodeFromIndex:(const bookmarks::BookmarkNode*)node {
DCHECK(node->is_url());
const GURL URL(node->url());
NSString* title = base::SysUTF16ToNSString(node->GetTitle());
NSString* spotlightID = [self.searchableItemFactory spotlightIDForURL:URL
title:title];
__weak BookmarksSpotlightManager* weakSelf = self;
[self.spotlightInterface
deleteSearchableItemsWithIdentifiers:@[ spotlightID ]
completionHandler:^(NSError*) {
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf onCompletedDeleteItemsWithURL:URL
title:title];
});
}];
}
// Completion helper for URL node deletion.
- (void)onCompletedDeleteItemsWithURL:(const GURL&)URL title:(NSString*)title {
[self refreshItemWithURL:URL title:title];
}
- (BOOL)shouldReindex {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
NSDate* date = base::apple::ObjCCast<NSDate>(
[userDefaults objectForKey:@(spotlight::kSpotlightLastIndexingDateKey)]);
if (!date) {
return YES;
}
const base::TimeDelta timeSinceLastIndexing =
base::Time::Now() - base::Time::FromNSDate(date);
if (timeSinceLastIndexing >= kDelayBetweenTwoIndexing) {
return YES;
}
NSNumber* lastIndexedVersion = base::apple::ObjCCast<NSNumber>([userDefaults
objectForKey:@(spotlight::kSpotlightLastIndexingVersionKey)]);
if (!lastIndexedVersion) {
return YES;
}
if ([lastIndexedVersion integerValue] <
spotlight::kCurrentSpotlightIndexVersion) {
return YES;
}
return NO;
}
- (void)reindexBookmarksIfNeeded {
if (!_bookmarkModel->loaded() || _initialIndexDone) {
return;
}
_initialIndexDone = YES;
if ([self shouldReindex]) {
[self clearAndReindexModel];
}
}
// Refresh any bookmark nodes matching given URL and title. If there are
// multiple nodes with same URL and title, they will be merged into a single
// spotlight item but will have tags from each of the bookmrk nodes.
- (void)refreshItemWithURL:(const GURL&)URL title:(NSString*)title {
std::vector<const bookmarks::BookmarkNode*> nodesMatchingURL;
_bookmarkModel->GetNodesByURL(URL, &nodesMatchingURL);
NSMutableArray* itemKeywords = [[NSMutableArray alloc] init];
// If there are no bookmarks nodes matching the url and title then we should
// make sure to not create and index a spotlight item with the given url and
// title.
BOOL shouldIndexItem = false;
// Build a list of tags for every node having the URL and title. Combine the
// lists of tags into one, that will be used to search for the spotlight item.
for (const bookmarks::BookmarkNode* node : nodesMatchingURL) {
NSString* nodeTitle = base::SysUTF16ToNSString(node->GetTitle());
if ([nodeTitle isEqualToString:title] == NO) {
continue;
}
/// there still a bookmark node that matches the given URL and title, so we
/// should refresh/reindex it in spotlight.
shouldIndexItem = true;
[itemKeywords addObjectsFromArray:[self parentFolderNamesForNode:node]];
}
if (shouldIndexItem) {
__weak BookmarksSpotlightManager* weakSelf = self;
_pendingLargeIconTasksCount++;
[self.searchableItemFactory
generateSearchableItem:URL
title:title
additionalKeywords:itemKeywords
completionHandler:^(CSSearchableItem* item) {
weakSelf.pendingLargeIconTasksCount--;
[weakSelf.spotlightInterface indexSearchableItems:@[ item ]];
}];
}
}
- (void)refreshNodeInIndex:(const bookmarks::BookmarkNode*)node {
if (_nodesIndexed > kMaxInitialIndexSize) {
return;
}
if (node->is_url()) {
_nodesIndexed++;
[self refreshItemWithURL:node->url()
title:base::SysUTF16ToNSString(node->GetTitle())];
return;
}
for (const auto& child : node->children())
[self refreshNodeInIndex:child.get()];
}
- (void)shutdown {
[self detachBookmarkModel];
}
- (void)clearAndReindexModel {
__weak BookmarksSpotlightManager* weakSelf = self;
[self.spotlightInterface
deleteSearchableItemsWithDomainIdentifiers:@[
spotlight::StringFromSpotlightDomain(spotlight::DOMAIN_BOOKMARKS)
]
completionHandler:^(NSError* error) {
if (error) {
[SpotlightLogger logSpotlightError:error];
return;
}
[weakSelf completedClearAllSpotlightItems];
}];
}
- (void)completedClearAllSpotlightItems {
const base::Time startOfReindexing = base::Time::Now();
_nodesIndexed = 0;
_pendingLargeIconTasksCount = 0;
[self refreshNodeInIndex:_bookmarkModel->root_node()];
const base::Time endOfReindexing = base::Time::Now();
UMA_HISTOGRAM_TIMES("IOS.Spotlight.BookmarksIndexingDuration",
endOfReindexing - startOfReindexing);
UMA_HISTOGRAM_COUNTS_1000("IOS.Spotlight.BookmarksInitialIndexSize",
_pendingLargeIconTasksCount);
[[NSUserDefaults standardUserDefaults]
setObject:endOfReindexing.ToNSDate()
forKey:@(spotlight::kSpotlightLastIndexingDateKey)];
[[NSUserDefaults standardUserDefaults]
setObject:@(spotlight::kCurrentSpotlightIndexVersion)
forKey:@(spotlight::kSpotlightLastIndexingVersionKey)];
}
@end