blob: 8a20fba26fc9cbca12486e1f6c65dcfcaf264635 [file] [log] [blame]
// Copyright 2015 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/app/spotlight/base_spotlight_manager.h"
#import <CommonCrypto/CommonCrypto.h>
#import <MobileCoreServices/MobileCoreServices.h>
#include "base/bind.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/cancelable_task_tracker.h"
#include "components/favicon/core/fallback_url_util.h"
#include "components/favicon/core/large_icon_service.h"
#include "components/favicon_base/fallback_icon_style.h"
#include "components/favicon_base/favicon_types.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ios/public/provider/chrome/browser/chrome_browser_provider.h"
#include "ios/public/provider/chrome/browser/spotlight/spotlight_provider.h"
#import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h"
#import "net/base/mac/url_conversions.h"
#include "skia/ext/skia_utils_ios.h"
#include "ui/base/l10n/l10n_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Minimum size of the icon to be used in Spotlight.
const NSInteger kMinIconSize = 32;
// Preferred size of the icon to be used in Spotlight.
const NSInteger kIconSize = 64;
// Size of the fallback icon.
const CGFloat kFallbackIconSize = 180;
// Radius of the rounded corner of the fallback icon.
const CGFloat kFallbackRoundedCorner = 8;
}
@interface BaseSpotlightManager () {
// Domain of the spotlight manager.
spotlight::Domain _spotlightDomain;
// Service to retrieve large favicon or colors for a fallback icon.
favicon::LargeIconService* _largeIconService; // weak
// Queue to query large icons.
base::CancelableTaskTracker _largeIconTaskTracker;
// Dictionary to track the tasks querying the large icons.
NSMutableDictionary* _pendingTasks;
// Records whether -shutdown has been invoked and the method forwarded to
// the base class.
BOOL _shutdownCalled;
}
// Compute a hash consisting of the first 8 bytes of the MD5 hash of a string
// containing |URL| and |title|.
- (int64_t)getHashForURL:(const GURL&)URL title:(NSString*)title;
// Create an image with a rounded square with color |backgroundColor| and
// |string| centered in color |textColor|.
UIImage* GetFallbackImageWithStringAndColor(NSString* string,
UIColor* backgroundColor,
UIColor* textColor);
// Returns an array of Keywords for Spotlight search.
- (NSArray*)keywordsForSpotlightItems;
@end
@implementation BaseSpotlightManager
- (instancetype)initWithLargeIconService:
(favicon::LargeIconService*)largeIconService
domain:(spotlight::Domain)domain {
self = [super init];
if (self) {
_spotlightDomain = domain;
_largeIconService = largeIconService;
_pendingTasks = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)dealloc {
DCHECK(_shutdownCalled);
}
- (int64_t)getHashForURL:(const GURL&)URL title:(NSString*)title {
NSString* key = [NSString
stringWithFormat:@"%@ %@", base::SysUTF8ToNSString(URL.spec()), title];
unsigned char hash[CC_MD5_DIGEST_LENGTH];
const std::string clipboard = base::SysNSStringToUTF8(key);
const char* c_string = clipboard.c_str();
CC_MD5(c_string, strlen(c_string), hash);
uint64_t md5 = *(reinterpret_cast<uint64_t*>(hash));
return md5;
}
- (NSString*)spotlightIDForURL:(const GURL&)URL title:(NSString*)title {
NSString* spotlightID = [NSString
stringWithFormat:@"%@.%016llx",
spotlight::StringFromSpotlightDomain(_spotlightDomain),
[self getHashForURL:URL title:title]];
return spotlightID;
}
- (void)cancelAllLargeIconPendingTasks {
_largeIconTaskTracker.TryCancelAll();
[_pendingTasks removeAllObjects];
}
- (void)clearAllSpotlightItems:(BlockWithError)callback {
[self cancelAllLargeIconPendingTasks];
spotlight::DeleteSearchableDomainItems(_spotlightDomain, callback);
}
- (CSSearchableItem*)spotlightItemWithItemID:(NSString*)itemID
attributeSet:(CSSearchableItemAttributeSet*)
attributeSet {
CSCustomAttributeKey* key = [[CSCustomAttributeKey alloc]
initWithKeyName:spotlight::GetSpotlightCustomAttributeItemID()
searchable:YES
searchableByDefault:YES
unique:YES
multiValued:NO];
[attributeSet setValue:itemID forCustomKey:key];
attributeSet.keywords = [self keywordsForSpotlightItems];
NSString* domainID = spotlight::StringFromSpotlightDomain(_spotlightDomain);
return [[CSSearchableItem alloc] initWithUniqueIdentifier:itemID
domainIdentifier:domainID
attributeSet:attributeSet];
}
- (NSArray*)spotlightItemsWithURL:(const GURL&)indexedURL
favicon:(UIImage*)favicon
defaultTitle:(NSString*)defaultTitle {
DCHECK(defaultTitle);
NSURL* nsURL = net::NSURLWithGURL(indexedURL);
std::string description = indexedURL.SchemeIsCryptographic()
? indexedURL.GetOrigin().spec()
: indexedURL.spec();
CSSearchableItemAttributeSet* attributeSet =
[[CSSearchableItemAttributeSet alloc]
initWithItemContentType:(NSString*)kUTTypeURL];
[attributeSet setTitle:defaultTitle];
[attributeSet setDisplayName:defaultTitle];
[attributeSet setURL:nsURL];
[attributeSet setContentURL:nsURL];
[attributeSet setContentDescription:base::SysUTF8ToNSString(description)];
[attributeSet setThumbnailData:UIImagePNGRepresentation(favicon)];
NSString* itemID = [self spotlightIDForURL:indexedURL title:defaultTitle];
return [NSArray arrayWithObject:[self spotlightItemWithItemID:itemID
attributeSet:attributeSet]];
}
UIImage* GetFallbackImageWithStringAndColor(NSString* string,
UIColor* backgroundColor,
UIColor* textColor) {
CGRect rect = CGRectMake(0, 0, kFallbackIconSize, kFallbackIconSize);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [backgroundColor CGColor]);
CGContextSetStrokeColorWithColor(context, [textColor CGColor]);
UIBezierPath* rounded =
[UIBezierPath bezierPathWithRoundedRect:rect
cornerRadius:kFallbackRoundedCorner];
[rounded fill];
UIFont* font = [MDCTypography headlineFont];
font = [font fontWithSize:(kFallbackIconSize / 2)];
CGRect textRect = CGRectMake(0, (kFallbackIconSize - [font lineHeight]) / 2,
kFallbackIconSize, [font lineHeight]);
NSMutableParagraphStyle* paragraphStyle =
[[NSMutableParagraphStyle alloc] init];
[paragraphStyle setAlignment:NSTextAlignmentCenter];
NSMutableDictionary* attributes = [[NSMutableDictionary alloc] init];
[attributes setValue:font forKey:NSFontAttributeName];
[attributes setValue:textColor forKey:NSForegroundColorAttributeName];
[attributes setValue:paragraphStyle forKey:NSParagraphStyleAttributeName];
[string drawInRect:textRect withAttributes:attributes];
UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
- (void)refreshItemsWithURL:(const GURL&)URLToRefresh title:(NSString*)title {
NSURL* NSURL = net::NSURLWithGURL(URLToRefresh);
if (!NSURL || [_pendingTasks objectForKey:NSURL]) {
return;
}
__weak BaseSpotlightManager* weakSelf = self;
GURL URL = URLToRefresh;
void (^faviconBlock)(const favicon_base::LargeIconResult&) = ^(
const favicon_base::LargeIconResult& result) {
BaseSpotlightManager* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
[strongSelf->_pendingTasks removeObjectForKey:NSURL];
UIImage* favicon;
if (result.bitmap.is_valid()) {
scoped_refptr<base::RefCountedMemory> data =
result.bitmap.bitmap_data.get();
favicon = [UIImage
imageWithData:[NSData dataWithBytes:data->front() length:data->size()]
scale:[UIScreen mainScreen].scale];
} else {
NSString* iconText =
base::SysUTF16ToNSString(favicon::GetFallbackIconText(URL));
UIColor* backgroundColor = skia::UIColorFromSkColor(
result.fallback_icon_style->background_color);
UIColor* textColor =
skia::UIColorFromSkColor(result.fallback_icon_style->text_color);
favicon = GetFallbackImageWithStringAndColor(iconText, backgroundColor,
textColor);
}
NSArray* spotlightItems = [strongSelf spotlightItemsWithURL:URL
favicon:favicon
defaultTitle:title];
if ([spotlightItems count]) {
[[CSSearchableIndex defaultSearchableIndex]
indexSearchableItems:spotlightItems
completionHandler:nil];
}
};
base::CancelableTaskTracker::TaskId taskID =
_largeIconService->GetLargeIconOrFallbackStyle(
URL, kMinIconSize * [UIScreen mainScreen].scale,
kIconSize * [UIScreen mainScreen].scale,
base::BindRepeating(faviconBlock), &_largeIconTaskTracker);
[_pendingTasks setObject:[NSNumber numberWithLongLong:taskID] forKey:NSURL];
}
- (NSUInteger)pendingLargeIconTasksCount {
return [_pendingTasks count];
}
- (void)shutdown {
[self cancelAllLargeIconPendingTasks];
_largeIconService = nullptr;
_shutdownCalled = YES;
}
#pragma mark private methods
- (NSArray*)keywordsForSpotlightItems {
NSMutableArray* keywordsArray = [NSMutableArray arrayWithArray:@[
l10n_util::GetNSString(IDS_IOS_SPOTLIGHT_KEYWORD_ONE),
l10n_util::GetNSString(IDS_IOS_SPOTLIGHT_KEYWORD_TWO),
l10n_util::GetNSString(IDS_IOS_SPOTLIGHT_KEYWORD_THREE),
l10n_util::GetNSString(IDS_IOS_SPOTLIGHT_KEYWORD_FOUR),
l10n_util::GetNSString(IDS_IOS_SPOTLIGHT_KEYWORD_FIVE),
l10n_util::GetNSString(IDS_IOS_SPOTLIGHT_KEYWORD_SIX),
l10n_util::GetNSString(IDS_IOS_SPOTLIGHT_KEYWORD_SEVEN),
l10n_util::GetNSString(IDS_IOS_SPOTLIGHT_KEYWORD_EIGHT),
l10n_util::GetNSString(IDS_IOS_SPOTLIGHT_KEYWORD_NINE),
l10n_util::GetNSString(IDS_IOS_SPOTLIGHT_KEYWORD_TEN)
]];
NSArray* additionalArray = ios::GetChromeBrowserProvider()
->GetSpotlightProvider()
->GetAdditionalKeywords();
if (additionalArray) {
[keywordsArray addObjectsFromArray:additionalArray];
}
return keywordsArray;
}
@end