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