chromium/ios/chrome/app/spotlight/spotlight_util.mm

// 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/spotlight_util.h"

#import <CoreSpotlight/CoreSpotlight.h>

#import "base/metrics/histogram_macros.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "build/branding_buildflags.h"
#import "ios/chrome/app/spotlight/spotlight_logger.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"

namespace {
// This enum is used for Histogram. Items should not be removed or reordered and
// this enum should be kept synced with histograms.xml.
// The three states correspond to:
// - SPOTLIGHT_UNSUPPORTED: Framework CoreSpotlight is not found and
// [CSSearchableIndex class] returns nil.
// - SPOTLIGHT_UNAVAILABLE: Framework is loaded but [CSSearchableIndex
// isIndexingAvailable] return NO. Note: It is unclear if this state is
// reachable (I could not find configuration where CoreSpotlight was loaded but
// [CSSearchableIndex isIndexingAvailable] returned NO.
// - SPOTLIGHT_AVAILABLE: Framework is loaded and [CSSearchableIndex
// isIndexingAvailable] returns YES. Note: This does not mean the actual
// indexing will happen. If the user disables Spotlight in the system settings,
// [CSSearchableIndex isIndexingAvailable] still returns YES.
enum Availability {
  SPOTLIGHT_UNSUPPORTED = 0,
  SPOTLIGHT_UNAVAILABLE,
  SPOTLIGHT_AVAILABLE,
  SPOTLIGHT_AVAILABILITY_COUNT
};

// Strings corresponding to the domain/prefix for respectively bookmarks,
// top sites and actions items for spotlight.
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
NSString* const kSpotlightBookmarkDomain = @"com.google.chrome.bookmarks";
NSString* const kSpotlightBookmarkPrefix = @"com.google.chrome.bookmarks.";

NSString* const kSpotlightTopSitesDomain = @"com.google.chrome.topsites";
NSString* const kSpotlightTopSitesPrefix = @"com.google.chrome.topsites.";

NSString* const kSpotlightActionsDomain = @"com.google.chrome.actions";
NSString* const kSpotlightActionsPrefix = @"com.google.chrome.actions.";

NSString* const kSpotlightReadingListDomain = @"com.google.readinglist";
NSString* const kSpotlightReadingListPrefix = @"com.google.readinglist.";

NSString* const kSpotlightOpenTabsDomain = @"com.google.opentabs";
NSString* const kSpotlightOpenTabsPrefix = @"com.google.opentabs.";
#else

NSString* const kSpotlightBookmarkDomain = @"org.chromium.bookmarks";
NSString* const kSpotlightBookmarkPrefix = @"org.chromium.bookmarks.";

NSString* const kSpotlightTopSitesDomain = @"org.chromium.topsites";
NSString* const kSpotlightTopSitesPrefix = @"org.chromium.topsites.";

NSString* const kSpotlightActionsDomain = @"org.chromium.actions";
NSString* const kSpotlightActionsPrefix = @"org.chromium.actions.";

NSString* const kSpotlightReadingListDomain = @"org.chromium.readinglist";
NSString* const kSpotlightReadingListPrefix = @"org.chromium.readinglist.";

NSString* const kSpotlightOpenTabsDomain = @"org.chromium.opentabs";
NSString* const kSpotlightOpenTabsPrefix = @"org.chromium.opentabs.";

#endif

}  // namespace

namespace spotlight {

// NSUserDefaults key of entry containing date of the latest bookmarks indexing.
const char kSpotlightLastIndexingDateKey[] = "SpotlightLastIndexingDate";

// NSUserDefault key of entry containing Chrome version of the latest bookmarks
// indexing.
const char kSpotlightLastIndexingVersionKey[] = "SpotlightLastIndexingVersion";

// The current version of the Spotlight index format.
// Change this value if there are change int the information indexed in
// Spotlight. This will force reindexation on next startup.
// Value is stored in `kSpotlightLastIndexingVersionKey`.
const int kCurrentSpotlightIndexVersion = 4;

Domain SpotlightDomainFromString(NSString* domain) {
  if ([domain hasPrefix:kSpotlightBookmarkPrefix]) {
    return DOMAIN_BOOKMARKS;
  } else if ([domain hasPrefix:kSpotlightTopSitesPrefix]) {
    return DOMAIN_TOPSITES;
  } else if ([domain hasPrefix:kSpotlightActionsPrefix]) {
    return DOMAIN_ACTIONS;
  } else if ([domain hasPrefix:kSpotlightReadingListPrefix]) {
    return DOMAIN_READING_LIST;
  } else if ([domain hasPrefix:kSpotlightOpenTabsPrefix]) {
    return DOMAIN_OPEN_TABS;
  }
  return DOMAIN_UNKNOWN;
}

NSString* StringFromSpotlightDomain(Domain domain) {
  switch (domain) {
    case DOMAIN_BOOKMARKS:
      return kSpotlightBookmarkDomain;
    case DOMAIN_TOPSITES:
      return kSpotlightTopSitesDomain;
    case DOMAIN_ACTIONS:
      return kSpotlightActionsDomain;
    case DOMAIN_READING_LIST:
      return kSpotlightReadingListDomain;
    case DOMAIN_OPEN_TABS:
      return kSpotlightOpenTabsDomain;

    default:
      // On normal flow, it is not possible to reach this point. When testing
      // the app, it may be possible though if the app is downgraded.
      NOTREACHED_IN_MIGRATION();
      return nil;
  }
}

NSString* SpotlightItemSourceLabelFromDomain(Domain domain) {
  switch (domain) {
    case DOMAIN_BOOKMARKS:
      return l10n_util::GetNSString(IDS_IOS_TOOLS_MENU_BOOKMARKS);
    case DOMAIN_TOPSITES:
      return l10n_util::GetNSString(
          IDS_IOS_CONTENT_SUGGESTIONS_MOST_VISITED_MODULE_TITLE);
    case DOMAIN_ACTIONS:
      return l10n_util::GetNSString(IDS_IOS_SPOTLIGHT_CHROME_ACTIONS_LABEL);
    case DOMAIN_READING_LIST:
      return l10n_util::GetNSString(IDS_IOS_TOOLS_MENU_READING_LIST);
    case DOMAIN_OPEN_TABS:
      return l10n_util::GetNSString(IDS_IOS_SPOTLIGHT_OPEN_TAB_LABEL);

    default:
      // On normal flow, it is not possible to reach this point. When testing
      // the app, it may be possible though if the app is downgraded.
      NOTREACHED_IN_MIGRATION();
      return nil;
  }
}

bool IsSpotlightAvailable() {
  bool loaded = !![CSSearchableIndex class];
  bool available = loaded && [CSSearchableIndex isIndexingAvailable];
  static dispatch_once_t once;
  dispatch_once(&once, ^{
    Availability availability = SPOTLIGHT_UNSUPPORTED;
    if (loaded) {
      availability = SPOTLIGHT_UNAVAILABLE;
    }
    if (available) {
      availability = SPOTLIGHT_AVAILABLE;
    }
    UMA_HISTOGRAM_ENUMERATION("IOS.Spotlight.Availability", availability,
                              SPOTLIGHT_AVAILABILITY_COUNT);
  });
  return loaded && available;
}

NSString* GetSpotlightCustomAttributeItemID() {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  return @"ComGoogleChromeItemID";
#else
  return @"OrgChromiumItemID";
#endif
}

void GetURLForSpotlightItemID(NSString* itemID, BlockWithNSURL completion) {
  NSString* queryString =
      [NSString stringWithFormat:@"%@ == \"%@\"",
                                 GetSpotlightCustomAttributeItemID(), itemID];

  CSSearchQueryContext* context = [[CSSearchQueryContext alloc] init];
  context.fetchAttributes = @[ @"contentURL" ];
  CSSearchQuery* query = [[CSSearchQuery alloc] initWithQueryString:queryString
                                                       queryContext:context];

  [query setFoundItemsHandler:^(NSArray<CSSearchableItem*>* items) {
    if ([items count] == 1) {
      CSSearchableItem* searchableItem = [items objectAtIndex:0];
      if (searchableItem) {
        completion([[searchableItem attributeSet] contentURL]);
        return;
      }
    }
    completion(nil);

  }];

  [query start];
}

}  // namespace spotlight