[go: nahoru, domu]

blob: 23dc937086ee282b3118f87b4e75dac0f11766a8 [file] [log] [blame]
// Copyright 2012 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/browser/snapshots/snapshot_cache.h"
#import "ios/chrome/browser/snapshots/snapshot_cache_internal.h"
#import <UIKit/UIKit.h>
#import <set>
#import "base/apple/backup_util.h"
#import "base/apple/foundation_util.h"
#import "base/base_paths.h"
#import "base/containers/contains.h"
#import "base/files/file_enumerator.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/functional/bind.h"
#import "base/ios/crb_protocol_observers.h"
#import "base/logging.h"
#import "base/metrics/histogram_functions.h"
#import "base/path_service.h"
#import "base/sequence_checker.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/sequenced_task_runner.h"
#import "base/task/thread_pool.h"
#import "base/threading/scoped_blocking_call.h"
#import "base/time/time.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/snapshots/snapshot_cache_observer.h"
#import "ios/chrome/browser/snapshots/snapshot_lru_cache.h"
#import "ios/chrome/browser/tabs/features.h"
#import "ui/base/device_form_factor.h"
// Protocol observers subclass that explicitly implements
// <SnapshotCacheObserver>.
@interface SnapshotCacheObservers : CRBProtocolObservers<SnapshotCacheObserver>
+ (instancetype)observers;
@end
@implementation SnapshotCacheObservers
+ (instancetype)observers {
return [self observersWithProtocol:@protocol(SnapshotCacheObserver)];
}
@end
@interface SnapshotCache ()
// List of observers to be notified of changes to the snapshot cache.
@property(nonatomic, strong) SnapshotCacheObservers* observers;
@end
namespace {
enum ImageType {
IMAGE_TYPE_COLOR,
IMAGE_TYPE_GREYSCALE,
};
enum ImageScale {
IMAGE_SCALE_1X,
IMAGE_SCALE_2X,
IMAGE_SCALE_3X,
};
const ImageType kImageTypes[] = {
IMAGE_TYPE_COLOR, IMAGE_TYPE_GREYSCALE,
};
const NSUInteger kGreyInitialCapacity = 8;
const CGFloat kJPEGImageQuality = 1.0; // Highest quality. No compression.
// Maximum size in number of elements that the LRU cache can hold before
// starting to evict elements.
const NSUInteger kLRUCacheMaxCapacity = 6;
// Maximum size in number of elements that the LRU cache can hold before
// starting to evict elements when PinnedTabs feature is enabled.
//
// To calculate the cache size number we'll start with the assumption that
// currently snapshot preloading feature "works fine". In the reality it might
// not be the case for large screen devices such as iPad. Another assumption
// here is that pinned tabs feature requires on average 4 more snapshots to be
// used. Based on that kLRUCacheMaxCapacityForPinnedTabsEnabled is
// kLRUCacheMaxCapacity which "works fine" + on average 4 more snapshots needed
// for pinned tabs feature.
const NSUInteger kLRUCacheMaxCapacityForPinnedTabsEnabled = 10;
// Returns the path of the image for `snapshot_id`, in `cache_directory`,
// of type `image_type` and scale `image_scale`.
base::FilePath ImagePath(NSString* snapshot_id,
ImageType image_type,
ImageScale image_scale,
const base::FilePath& cache_directory) {
NSString* filename = snapshot_id;
switch (image_type) {
case IMAGE_TYPE_COLOR:
// no-op
break;
case IMAGE_TYPE_GREYSCALE:
filename = [filename stringByAppendingString:@"Grey"];
break;
}
switch (image_scale) {
case IMAGE_SCALE_1X:
// no-op
break;
case IMAGE_SCALE_2X:
filename = [filename stringByAppendingString:@"@2x"];
break;
case IMAGE_SCALE_3X:
filename = [filename stringByAppendingString:@"@3x"];
break;
}
filename = [filename stringByAppendingPathExtension:@"jpg"];
return cache_directory.Append(base::SysNSStringToUTF8(filename));
}
ImageScale ImageScaleForDevice() {
// On handset, the color snapshot is used for the stack view, so the scale of
// the snapshot images should match the scale of the device.
// On tablet, the color snapshot is only used to generate the grey snapshot,
// which does not have to be high quality, so use scale of 1.0 on all tablets.
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET)
return IMAGE_SCALE_1X;
// Cap snapshot resolution to 2x to reduce the amount of memory used.
return [UIScreen mainScreen].scale == 1.0 ? IMAGE_SCALE_1X : IMAGE_SCALE_2X;
}
CGFloat ScaleFromImageScale(ImageScale image_scale) {
switch (image_scale) {
case IMAGE_SCALE_1X:
return 1.0;
case IMAGE_SCALE_2X:
return 2.0;
case IMAGE_SCALE_3X:
return 3.0;
}
}
UIImage* ReadImageForSnapshotIDFromDisk(NSString* snapshot_id,
ImageType image_type,
ImageScale image_scale,
const base::FilePath& cache_directory) {
// TODO(crbug.com/295891): consider changing back to -imageWithContentsOfFile
// instead of -imageWithData if both rdar://15747161 and the bug incorrectly
// reporting the image as damaged https://stackoverflow.com/q/5081297/5353
// are fixed.
base::FilePath file_path =
ImagePath(snapshot_id, image_type, image_scale, cache_directory);
NSString* path = base::apple::FilePathToNSString(file_path);
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
return [UIImage imageWithData:[NSData dataWithContentsOfFile:path]
scale:(image_type == IMAGE_TYPE_GREYSCALE
? 1.0
: ScaleFromImageScale(image_scale))];
}
void WriteImageToDisk(UIImage* image, const base::FilePath& file_path) {
if (!image)
return;
// CGImage should exist, otherwise UIImageJPEG(PNG)Representation returns nil.
CHECK(image.CGImage);
base::FilePath directory = file_path.DirName();
if (!base::DirectoryExists(directory)) {
bool success = base::CreateDirectory(directory);
if (!success) {
DLOG(ERROR) << "Error creating thumbnail directory "
<< directory.AsUTF8Unsafe();
return;
}
}
NSString* path = base::apple::FilePathToNSString(file_path);
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
NSData* data = UIImageJPEGRepresentation(image, kJPEGImageQuality);
if (!data) {
// Use UIImagePNGRepresentation instead when ImageJPEGRepresentation returns
// nil. It happens when the underlying CGImageRef contains data in an
// unsupported bitmap format.
data = UIImagePNGRepresentation(image);
}
[data writeToFile:path atomically:YES];
// Encrypt the snapshot file (mostly for Incognito, but can't hurt to
// always do it).
NSDictionary* attribute_dict = [NSDictionary
dictionaryWithObject:NSFileProtectionCompleteUntilFirstUserAuthentication
forKey:NSFileProtectionKey];
NSError* error = nil;
BOOL success = [[NSFileManager defaultManager] setAttributes:attribute_dict
ofItemAtPath:path
error:&error];
if (!success) {
DLOG(ERROR) << "Error encrypting thumbnail file "
<< base::SysNSStringToUTF8([error description]);
}
}
void ConvertAndSaveGreyImage(NSString* snapshot_id,
ImageScale image_scale,
UIImage* color_image,
const base::FilePath& cache_directory) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
if (!color_image) {
color_image = ReadImageForSnapshotIDFromDisk(snapshot_id, IMAGE_TYPE_COLOR,
image_scale, cache_directory);
if (!color_image)
return;
}
UIImage* grey_image = GreyImage(color_image);
base::FilePath image_path = ImagePath(snapshot_id, IMAGE_TYPE_GREYSCALE,
image_scale, cache_directory);
WriteImageToDisk(grey_image, image_path);
base::apple::SetBackupExclusion(image_path);
}
void DeleteImageWithSnapshotID(const base::FilePath& cache_directory,
NSString* snapshot_id,
ImageScale snapshot_scale) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
for (const ImageType image_type : kImageTypes) {
base::DeleteFile(
ImagePath(snapshot_id, image_type, snapshot_scale, cache_directory));
}
}
void RemoveAllImages(const base::FilePath& cache_directory) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
if (cache_directory.empty() || !base::DirectoryExists(cache_directory))
return;
if (!base::DeletePathRecursively(cache_directory)) {
DLOG(ERROR) << "Error deleting snapshots storage. "
<< cache_directory.AsUTF8Unsafe();
}
if (!base::CreateDirectory(cache_directory)) {
DLOG(ERROR) << "Error creating snapshot storage "
<< cache_directory.AsUTF8Unsafe();
}
}
void PurgeCacheOlderThan(const base::FilePath& cache_directory,
const base::Time& threshold_date,
NSSet<NSString*>* keep_alive_snapshot_ids,
ImageScale snapshot_scale) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
if (!base::DirectoryExists(cache_directory))
return;
std::set<base::FilePath> files_to_keep;
for (NSString* snapshot_id in keep_alive_snapshot_ids) {
for (const ImageType image_type : kImageTypes) {
files_to_keep.insert(
ImagePath(snapshot_id, image_type, snapshot_scale, cache_directory));
}
}
base::FileEnumerator enumerator(cache_directory, false,
base::FileEnumerator::FILES);
for (base::FilePath current_file = enumerator.Next(); !current_file.empty();
current_file = enumerator.Next()) {
if (current_file.Extension() != ".jpg")
continue;
if (base::Contains(files_to_keep, current_file))
continue;
base::FileEnumerator::FileInfo file_info = enumerator.GetInfo();
if (file_info.GetLastModifiedTime() > threshold_date)
continue;
base::DeleteFile(current_file);
}
}
void RenameSnapshots(const base::FilePath& cache_directory,
NSArray<NSString*>* old_ids,
NSArray<NSString*>* new_ids,
ImageScale snapshot_scale) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
DCHECK(base::DirectoryExists(cache_directory));
DCHECK_EQ(old_ids.count, new_ids.count);
const NSUInteger count = old_ids.count;
for (NSUInteger index = 0; index < count; ++index) {
for (const ImageType image_type : kImageTypes) {
const base::FilePath old_image_path = ImagePath(
old_ids[index], image_type, snapshot_scale, cache_directory);
const base::FilePath new_image_path = ImagePath(
new_ids[index], image_type, snapshot_scale, cache_directory);
// Only migrate snapshots that are needed.
if (!base::PathExists(old_image_path) ||
base::PathExists(new_image_path)) {
continue;
}
if (!base::Move(old_image_path, new_image_path)) {
DLOG(ERROR) << "Error migrating file: " << old_image_path.AsUTF8Unsafe()
<< " to: " << new_image_path.AsUTF8Unsafe();
}
}
}
}
void CopyImageFile(const base::FilePath& old_image_path,
const base::FilePath& new_image_path) {
// Only migrate files that are needed.
if (!base::PathExists(old_image_path) || base::PathExists(new_image_path)) {
return;
}
if (!base::CopyFile(old_image_path, new_image_path)) {
DLOG(ERROR) << "Error copying file: " << old_image_path.AsUTF8Unsafe()
<< " to: " << new_image_path.AsUTF8Unsafe();
}
}
void CreateCacheDirectory(const base::FilePath& cache_directory,
const base::FilePath& legacy_directory) {
// This is a NO-OP if the directory already exists.
if (!base::CreateDirectory(cache_directory)) {
const base::File::Error error = base::File::GetLastFileError();
DLOG(ERROR) << "Error creating snapshot storage: "
<< cache_directory.AsUTF8Unsafe() << ": "
<< base::File::ErrorToString(error);
return;
}
if (legacy_directory.empty() || !base::DirectoryExists(legacy_directory)) {
return;
}
// If `legacy_directory` exists and is a directory, move its content to
// `cache_directory` and then delete the directory. As this function is
// used to move snapshot file which are not stored recursively, limit
// the enumeration to files and do not perform a recursive enumeration.
base::FileEnumerator iter(legacy_directory, /*recursive*/ false,
base::FileEnumerator::FILES);
for (base::FilePath item = iter.Next(); !item.empty(); item = iter.Next()) {
base::FilePath to_path = cache_directory;
legacy_directory.AppendRelativePath(item, &to_path);
base::Move(item, to_path);
}
// Delete the `legacy_directory` once the existing files have been moved.
base::DeletePathRecursively(legacy_directory);
}
UIImage* GreyImageFromCachedImage(const base::FilePath& cache_directory,
NSString* snapshot_id,
ImageScale snapshot_scale,
UIImage* cached_image) {
// If the image is not in the cache, load it from disk.
UIImage* image = cached_image;
if (!image) {
image = ReadImageForSnapshotIDFromDisk(snapshot_id, IMAGE_TYPE_COLOR,
snapshot_scale, cache_directory);
}
if (!image)
return nil;
return GreyImage(image);
}
} // anonymous namespace
@implementation SnapshotCache {
// Cache to hold color snapshots in memory. n.b. Color snapshots are not
// kept in memory on tablets.
SnapshotLRUCache* _lruCache;
// Temporary dictionary to hold grey snapshots for tablet side swipe. This
// will be nil before -createGreyCache is called and after -removeGreyCache
// is called.
NSMutableDictionary<NSString*, UIImage*>* _greyImageDictionary;
// Snapshot ID of most recent pending grey snapshot request.
NSString* _mostRecentGreySnapshotID;
// Block used by pending request for a grey snapshot.
void (^_mostRecentGreyBlock)(UIImage*);
// Snapshot ID and corresponding UIImage for the snapshot that will likely
// be requested to be saved to disk when the application is backgrounded.
NSString* _backgroundingSnapshotID;
UIImage* _backgroundingColorImage;
// Scale for snapshot images. May be smaller than the screen scale in order
// to save memory on some devices.
ImageScale _snapshotsScale;
// Directory where the thumbnails are saved.
base::FilePath _cacheDirectory;
// Task runner used to run tasks in the background. Will be invalidated when
// -shutdown is invoked. Code should support this value to be null (generally
// by not posting the task).
scoped_refptr<base::SequencedTaskRunner> _taskRunner;
// Check that public API is called from the correct sequence.
SEQUENCE_CHECKER(_sequenceChecker);
}
- (instancetype)initWithStoragePath:(const base::FilePath&)storagePath
legacyPath:(const base::FilePath&)legacyPath {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if ((self = [super init])) {
NSUInteger cacheSize = IsPinnedTabsEnabled()
? kLRUCacheMaxCapacityForPinnedTabsEnabled
: kLRUCacheMaxCapacity;
_lruCache = [[SnapshotLRUCache alloc] initWithCacheSize:cacheSize];
_cacheDirectory = storagePath;
_snapshotsScale = ImageScaleForDevice();
_taskRunner = base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE});
_taskRunner->PostTask(
FROM_HERE,
base::BindOnce(CreateCacheDirectory, _cacheDirectory, legacyPath));
_observers = [SnapshotCacheObservers observers];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(handleLowMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(handleEnterBackground)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(handleBecomeActive)
name:UIApplicationDidBecomeActiveNotification
object:nil];
}
return self;
}
- (instancetype)initWithStoragePath:(const base::FilePath&)storagePath {
return [self initWithStoragePath:storagePath legacyPath:base::FilePath()];
}
- (void)dealloc {
DCHECK(!_taskRunner) << "-shutdown must be called before -dealloc";
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:UIApplicationDidBecomeActiveNotification
object:nil];
}
- (CGFloat)snapshotScaleForDevice {
return ScaleFromImageScale(_snapshotsScale);
}
- (void)retrieveImageForSnapshotID:(NSString*)snapshotID
callback:(void (^)(UIImage*))callback {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
DCHECK(snapshotID);
DCHECK(callback);
if (UIImage* image = [_lruCache objectForKey:snapshotID]) {
callback(image);
return;
}
if (!_taskRunner) {
callback(nil);
return;
}
__weak SnapshotLRUCache* weakLRUCache = _lruCache;
_taskRunner->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&ReadImageForSnapshotIDFromDisk, snapshotID,
IMAGE_TYPE_COLOR, _snapshotsScale, _cacheDirectory),
base::BindOnce(^(UIImage* image) {
if (image)
[weakLRUCache setObject:image forKey:snapshotID];
callback(image);
}));
}
- (void)setImage:(UIImage*)image withSnapshotID:(NSString*)snapshotID {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (!image || !snapshotID || !_taskRunner)
return;
[_lruCache setObject:image forKey:snapshotID];
// Each image in the cache has the same resolution and hence the same size.
size_t imageSizes = CGImageGetBytesPerRow(image.CGImage) *
CGImageGetHeight(image.CGImage) * [_lruCache count];
base::UmaHistogramMemoryKB("IOS.Snapshots.CacheSize", imageSizes / 1024);
[self.observers snapshotCache:self didUpdateSnapshotForID:snapshotID];
// Save the image to disk.
_taskRunner->PostTask(
FROM_HERE, base::BindOnce(&WriteImageToDisk, image,
ImagePath(snapshotID, IMAGE_TYPE_COLOR,
_snapshotsScale, _cacheDirectory)));
}
- (void)removeImageWithSnapshotID:(NSString*)snapshotID {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
[_lruCache removeObjectForKey:snapshotID];
[self.observers snapshotCache:self didUpdateSnapshotForID:snapshotID];
if (!_taskRunner)
return;
_taskRunner->PostTask(
FROM_HERE, base::BindOnce(&DeleteImageWithSnapshotID, _cacheDirectory,
snapshotID, _snapshotsScale));
}
- (void)removeAllImages {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
[_lruCache removeAllObjects];
if (!_taskRunner)
return;
_taskRunner->PostTask(FROM_HERE,
base::BindOnce(&RemoveAllImages, _cacheDirectory));
}
- (base::FilePath)imagePathForSnapshotID:(NSString*)snapshotID {
return ImagePath(snapshotID, IMAGE_TYPE_COLOR, _snapshotsScale,
_cacheDirectory);
}
- (base::FilePath)greyImagePathForSnapshotID:(NSString*)snapshotID {
return ImagePath(snapshotID, IMAGE_TYPE_GREYSCALE, _snapshotsScale,
_cacheDirectory);
}
- (void)purgeCacheOlderThan:(const base::Time&)date
keeping:(NSSet*)liveSnapshotIDs {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (!_taskRunner)
return;
_taskRunner->PostTask(
FROM_HERE, base::BindOnce(&PurgeCacheOlderThan, _cacheDirectory, date,
liveSnapshotIDs, _snapshotsScale));
}
- (void)renameSnapshotsWithIDs:(NSArray<NSString*>*)oldIDs
toIDs:(NSArray<NSString*>*)newIDs {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (!_taskRunner) {
return;
}
DCHECK_EQ(oldIDs.count, newIDs.count);
_taskRunner->PostTask(
FROM_HERE, base::BindOnce(&RenameSnapshots, _cacheDirectory, oldIDs,
newIDs, _snapshotsScale));
}
- (void)migrateImageWithSnapshotID:(NSString*)snapshotID
toSnapshotCache:(SnapshotCache*)destinationCache {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
// Copy to the destination cache.
if (UIImage* image = [_lruCache objectForKey:snapshotID]) {
// Copy both on-disk and in-memory versions.
[destinationCache setImage:image withSnapshotID:snapshotID];
// Copy the grey scale version, if available.
if (UIImage* greyImage = [_greyImageDictionary objectForKey:snapshotID]) {
[destinationCache->_greyImageDictionary setObject:greyImage
forKey:snapshotID];
}
} else {
// Only copy on-disk.
if (_taskRunner) {
_taskRunner->PostTask(
FROM_HERE,
base::BindOnce(&CopyImageFile,
[self imagePathForSnapshotID:snapshotID],
[destinationCache imagePathForSnapshotID:snapshotID]));
_taskRunner->PostTask(
FROM_HERE,
base::BindOnce(
&CopyImageFile, [self greyImagePathForSnapshotID:snapshotID],
[destinationCache greyImagePathForSnapshotID:snapshotID]));
}
}
// Remove the snapshot from this cache.
[self removeImageWithSnapshotID:snapshotID];
}
- (void)willBeSavedGreyWhenBackgrounding:(NSString*)snapshotID {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (!snapshotID)
return;
_backgroundingSnapshotID = [snapshotID copy];
_backgroundingColorImage = [_lruCache objectForKey:snapshotID];
}
// Remove all but adjacent UIImages from `lruCache_`.
- (void)handleLowMemory {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
NSMutableDictionary<NSString*, UIImage*>* dictionary =
[NSMutableDictionary dictionaryWithCapacity:2];
for (NSString* snapshotID in self.pinnedSnapshotIDs) {
UIImage* image = [_lruCache objectForKey:snapshotID];
if (image)
[dictionary setObject:image forKey:snapshotID];
}
[_lruCache removeAllObjects];
for (NSString* snapshotID in self.pinnedSnapshotIDs) {
[_lruCache setObject:[dictionary objectForKey:snapshotID]
forKey:snapshotID];
}
}
// Remove all UIImages from `lruCache_`.
- (void)handleEnterBackground {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
[_lruCache removeAllObjects];
}
// Restore adjacent UIImages to `lruCache_`.
- (void)handleBecomeActive {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
for (NSString* snapshotID in self.pinnedSnapshotIDs) {
[self retrieveImageForSnapshotID:snapshotID
callback:^(UIImage*){
}];
}
}
// Save grey image to `greyImageDictionary_` and call into most recent
// `_mostRecentGreyBlock` if `_mostRecentGreySnapshotID` matches `snapshotID`.
- (void)saveGreyImage:(UIImage*)greyImage forSnapshotID:(NSString*)snapshotID {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (greyImage)
[_greyImageDictionary setObject:greyImage forKey:snapshotID];
if ([snapshotID isEqualToString:_mostRecentGreySnapshotID]) {
_mostRecentGreyBlock(greyImage);
[self clearGreySnapshotInfo];
}
}
// Load uncached snapshot image and convert image to grey.
- (void)loadGreyImageAsync:(NSString*)snapshotID {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
// Don't call -retrieveImageForSnapshotID here because it caches the colored
// image, which we don't need for the grey image cache. But if the image is
// already in the cache, use it.
UIImage* image = [_lruCache objectForKey:snapshotID];
if (!_taskRunner)
return;
__weak SnapshotCache* weakSelf = self;
_taskRunner->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&GreyImageFromCachedImage, _cacheDirectory, snapshotID,
_snapshotsScale, image),
base::BindOnce(^(UIImage* greyImage) {
[weakSelf saveGreyImage:greyImage forSnapshotID:snapshotID];
}));
}
- (void)createGreyCache:(NSArray*)snapshotIDs {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
_greyImageDictionary =
[NSMutableDictionary dictionaryWithCapacity:kGreyInitialCapacity];
for (NSString* snapshotID in snapshotIDs)
[self loadGreyImageAsync:snapshotID];
}
- (void)removeGreyCache {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
_greyImageDictionary = nil;
[self clearGreySnapshotInfo];
}
// Clear most recent caller information.
- (void)clearGreySnapshotInfo {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
_mostRecentGreySnapshotID = nil;
_mostRecentGreyBlock = nil;
}
- (void)greyImageForSnapshotID:(NSString*)snapshotID
callback:(void (^)(UIImage*))callback {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
DCHECK(_greyImageDictionary);
DCHECK(snapshotID);
DCHECK(callback);
if (UIImage* image = [_greyImageDictionary objectForKey:snapshotID]) {
callback(image);
[self clearGreySnapshotInfo];
} else {
_mostRecentGreySnapshotID = [snapshotID copy];
_mostRecentGreyBlock = [callback copy];
}
}
- (void)retrieveGreyImageForSnapshotID:(NSString*)snapshotID
callback:(void (^)(UIImage*))callback {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
DCHECK(snapshotID);
DCHECK(callback);
if (_greyImageDictionary) {
if (UIImage* image = [_greyImageDictionary objectForKey:snapshotID]) {
callback(image);
return;
}
}
if (!_taskRunner) {
callback(nil);
return;
}
__weak SnapshotCache* weakSelf = self;
_taskRunner->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&ReadImageForSnapshotIDFromDisk, snapshotID,
IMAGE_TYPE_GREYSCALE, _snapshotsScale, _cacheDirectory),
base::BindOnce(^(UIImage* image) {
if (image) {
callback(image);
return;
}
[weakSelf retrieveImageForSnapshotID:snapshotID
callback:^(UIImage* snapshotImage) {
if (snapshotImage) {
snapshotImage =
GreyImage(snapshotImage);
}
callback(snapshotImage);
}];
}));
}
- (void)saveGreyInBackgroundForSnapshotID:(NSString*)snapshotID {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (!snapshotID)
return;
// The color image may still be in memory. Verify the snapshotID matches.
if (_backgroundingColorImage) {
if (![_backgroundingSnapshotID isEqualToString:snapshotID]) {
_backgroundingSnapshotID = nil;
_backgroundingColorImage = nil;
}
}
if (!_taskRunner)
return;
_taskRunner->PostTask(
FROM_HERE,
base::BindOnce(&ConvertAndSaveGreyImage, snapshotID, _snapshotsScale,
_backgroundingColorImage, _cacheDirectory));
}
- (void)addObserver:(id<SnapshotCacheObserver>)observer {
[self.observers addObserver:observer];
}
- (void)removeObserver:(id<SnapshotCacheObserver>)observer {
[self.observers removeObserver:observer];
}
- (void)shutdown {
_taskRunner = nullptr;
}
@end
@implementation SnapshotCache (TestingAdditions)
- (BOOL)hasImageInMemory:(NSString*)snapshotID {
return [_lruCache objectForKey:snapshotID] != nil;
}
- (BOOL)hasGreyImageInMemory:(NSString*)snapshotID {
return [_greyImageDictionary objectForKey:snapshotID] != nil;
}
- (NSUInteger)lruCacheMaxSize {
return [_lruCache maxCacheSize];
}
@end