1// Copyright 2015 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "ios/chrome/app/spotlight/spotlight_util.h" 6 7#import <CoreSpotlight/CoreSpotlight.h> 8 9#include "base/metrics/histogram_macros.h" 10#include "base/strings/sys_string_conversions.h" 11#include "ios/public/provider/chrome/browser/chrome_browser_provider.h" 12#include "ios/public/provider/chrome/browser/spotlight/spotlight_provider.h" 13#include "url/gurl.h" 14 15#if !defined(__has_feature) || !__has_feature(objc_arc) 16#error "This file requires ARC support." 17#endif 18 19namespace { 20// This enum is used for Histogram. Items should not be removed or reordered and 21// this enum should be kept synced with histograms.xml. 22// The three states correspond to: 23// - SPOTLIGHT_UNSUPPORTED: Framework CoreSpotlight is not found and 24// [CSSearchableIndex class] returns nil. 25// - SPOTLIGHT_UNAVAILABLE: Framework is loaded but [CSSearchableIndex 26// isIndexingAvailable] return NO. Note: It is unclear if this state is 27// reachable (I could not find configuration where CoreSpotlight was loaded but 28// [CSSearchableIndex isIndexingAvailable] returned NO. 29// - SPOTLIGHT_AVAILABLE: Framework is loaded and [CSSearchableIndex 30// isIndexingAvailable] returns YES. Note: This does not mean the actual 31// indexing will happen. If the user disables Spotlight in the system settings, 32// [CSSearchableIndex isIndexingAvailable] still returns YES. 33enum Availability { 34 SPOTLIGHT_UNSUPPORTED = 0, 35 SPOTLIGHT_UNAVAILABLE, 36 SPOTLIGHT_AVAILABLE, 37 SPOTLIGHT_AVAILABILITY_COUNT 38}; 39 40// Documentation says that failed deletion should be retried. Set a maximum 41// value to avoid infinite loop. 42const int kMaxDeletionAttempts = 5; 43 44// Execute blockName block with up to retryCount retries on error. Execute 45// callback when done. 46void DoWithRetry(BlockWithError callback, 47 NSUInteger retryCount, 48 void (^blockName)(BlockWithError error)) { 49 BlockWithError retryCallback = ^(NSError* error) { 50 if (error && retryCount > 0) { 51 DoWithRetry(callback, retryCount - 1, blockName); 52 } else { 53 if (callback) { 54 callback(error); 55 } 56 } 57 }; 58 blockName(retryCallback); 59} 60 61// Execute blockName block with up to kMaxDeletionAttempts retries on error. 62// Execute callback when done. 63void DoWithRetry(BlockWithError completion, 64 void (^blockName)(BlockWithError error)) { 65 DoWithRetry(completion, kMaxDeletionAttempts, blockName); 66} 67 68} // namespace 69 70namespace spotlight { 71 72// NSUserDefaults key of entry containing date of the latest bookmarks indexing. 73const char kSpotlightLastIndexingDateKey[] = "SpotlightLastIndexingDate"; 74 75// NSUserDefault key of entry containing Chrome version of the latest bookmarks 76// indexing. 77const char kSpotlightLastIndexingVersionKey[] = "SpotlightLastIndexingVersion"; 78 79// The current version of the Spotlight index format. 80// Change this value if there are change int the information indexed in 81// Spotlight. This will force reindexation on next startup. 82// Value is stored in |kSpotlightLastIndexingVersionKey|. 83const int kCurrentSpotlightIndexVersion = 3; 84 85Domain SpotlightDomainFromString(NSString* domain) { 86 SpotlightProvider* provider = 87 ios::GetChromeBrowserProvider()->GetSpotlightProvider(); 88 if ([domain hasPrefix:[provider->GetBookmarkDomain() 89 stringByAppendingString:@"."]]) { 90 return DOMAIN_BOOKMARKS; 91 } else if ([domain hasPrefix:[provider->GetTopSitesDomain() 92 stringByAppendingString:@"."]]) { 93 return DOMAIN_TOPSITES; 94 } else if ([domain hasPrefix:[provider->GetActionsDomain() 95 stringByAppendingString:@"."]]) { 96 return DOMAIN_ACTIONS; 97 } 98 // On normal flow, it is not possible to reach this point. When testing the 99 // app, it may be possible though if the app is downgraded. 100 NOTREACHED(); 101 return DOMAIN_UNKNOWN; 102} 103 104NSString* StringFromSpotlightDomain(Domain domain) { 105 SpotlightProvider* provider = 106 ios::GetChromeBrowserProvider()->GetSpotlightProvider(); 107 switch (domain) { 108 case DOMAIN_BOOKMARKS: 109 return provider->GetBookmarkDomain(); 110 case DOMAIN_TOPSITES: 111 return provider->GetTopSitesDomain(); 112 case DOMAIN_ACTIONS: 113 return provider->GetActionsDomain(); 114 default: 115 // On normal flow, it is not possible to reach this point. When testing 116 // the app, it may be possible though if the app is downgraded. 117 NOTREACHED(); 118 return nil; 119 } 120} 121 122void DeleteItemsWithIdentifiers(NSArray* items, BlockWithError callback) { 123 void (^deleteItems)(BlockWithError) = ^(BlockWithError errorBlock) { 124 [[CSSearchableIndex defaultSearchableIndex] 125 deleteSearchableItemsWithIdentifiers:items 126 completionHandler:errorBlock]; 127 }; 128 129 DoWithRetry(callback, deleteItems); 130} 131 132void DeleteSearchableDomainItems(Domain domain, BlockWithError callback) { 133 void (^deleteItems)(BlockWithError) = ^(BlockWithError errorBlock) { 134 [[CSSearchableIndex defaultSearchableIndex] 135 deleteSearchableItemsWithDomainIdentifiers:@[ StringFromSpotlightDomain( 136 domain) ] 137 completionHandler:errorBlock]; 138 }; 139 140 DoWithRetry(callback, deleteItems); 141} 142 143void ClearAllSpotlightEntries(BlockWithError callback) { 144 BlockWithError augmentedCallback = ^(NSError* error) { 145 [[NSUserDefaults standardUserDefaults] 146 removeObjectForKey:@(kSpotlightLastIndexingDateKey)]; 147 if (callback) { 148 callback(error); 149 } 150 }; 151 152 void (^deleteItems)(BlockWithError) = ^(BlockWithError errorBlock) { 153 [[CSSearchableIndex defaultSearchableIndex] 154 deleteAllSearchableItemsWithCompletionHandler:errorBlock]; 155 }; 156 157 DoWithRetry(augmentedCallback, deleteItems); 158} 159 160bool IsSpotlightAvailable() { 161 bool provided = ios::GetChromeBrowserProvider() 162 ->GetSpotlightProvider() 163 ->IsSpotlightEnabled(); 164 if (!provided) { 165 // The product does not support Spotlight, do not go further. 166 return false; 167 } 168 bool loaded = !![CSSearchableIndex class]; 169 bool available = loaded && [CSSearchableIndex isIndexingAvailable]; 170 static dispatch_once_t once; 171 dispatch_once(&once, ^{ 172 Availability availability = SPOTLIGHT_UNSUPPORTED; 173 if (loaded) { 174 availability = SPOTLIGHT_UNAVAILABLE; 175 } 176 if (available) { 177 availability = SPOTLIGHT_AVAILABLE; 178 } 179 UMA_HISTOGRAM_ENUMERATION("IOS.Spotlight.Availability", availability, 180 SPOTLIGHT_AVAILABILITY_COUNT); 181 }); 182 return loaded && available; 183} 184 185void ClearSpotlightIndexWithCompletion(BlockWithError completion) { 186 DCHECK(IsSpotlightAvailable()); 187 ClearAllSpotlightEntries(completion); 188} 189 190NSString* GetSpotlightCustomAttributeItemID() { 191 return ios::GetChromeBrowserProvider() 192 ->GetSpotlightProvider() 193 ->GetCustomAttributeItemID(); 194} 195 196void GetURLForSpotlightItemID(NSString* itemID, BlockWithNSURL completion) { 197 NSString* queryString = 198 [NSString stringWithFormat:@"%@ == \"%@\"", 199 GetSpotlightCustomAttributeItemID(), itemID]; 200 201 CSSearchQuery* query = 202 [[CSSearchQuery alloc] initWithQueryString:queryString 203 attributes:@[ @"contentURL" ]]; 204 205 [query setFoundItemsHandler:^(NSArray<CSSearchableItem*>* items) { 206 if ([items count] == 1) { 207 CSSearchableItem* searchableItem = [items objectAtIndex:0]; 208 if (searchableItem) { 209 completion([[searchableItem attributeSet] contentURL]); 210 return; 211 } 212 } 213 completion(nil); 214 215 }]; 216 217 [query start]; 218} 219 220} // namespace spotlight 221