Chromium Code Reviews| Index: ios/chrome/browser/snapshots/snapshot_cache.mm |
| diff --git a/ios/chrome/browser/snapshots/snapshot_cache.mm b/ios/chrome/browser/snapshots/snapshot_cache.mm |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..96fba90b5c8bc9e0f43dad185f851b2d7de89abb |
| --- /dev/null |
| +++ b/ios/chrome/browser/snapshots/snapshot_cache.mm |
| @@ -0,0 +1,466 @@ |
| +// Copyright 2012 The Chromium Authors. All rights reserved. |
|
Paweł Hajdan Jr.
2015/01/30 12:24:29
nit: 2015
|
| +// 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 <UIKit/UIKit.h> |
| + |
| +#include "base/critical_closure.h" |
| +#include "base/files/file_enumerator.h" |
| +#include "base/files/file_path.h" |
| +#include "base/files/file_util.h" |
| +#include "base/location.h" |
| +#include "base/logging.h" |
| +#include "base/mac/bind_objc_block.h" |
| +#include "base/mac/scoped_cftyperef.h" |
| +#include "base/strings/sys_string_conversions.h" |
| +#include "base/threading/thread_restrictions.h" |
| +#include "ios/chrome/browser/ui/ui_util.h" |
| +#import "ios/chrome/browser/ui/uikit_ui_util.h" |
| +#include "ios/public/consumer/base/util.h" |
| +#include "ios/web/public/web_thread.h" |
| + |
| +namespace { |
| +static NSArray* const kSnapshotCacheDirectory = @[ @"Chromium", @"Snapshots" ]; |
| + |
| +const NSUInteger kCacheInitialCapacity = 100; |
| +const NSUInteger kGreyInitialCapacity = 8; |
| +const CGFloat kJPEGImageQuality = 1.0; // Highest quality. No compression. |
| +// Sequence token to make sure creation/deletion of snapshots don't overlap. |
| +const char kSequenceToken[] = "SnapshotCacheSequenceToken"; |
| + |
| +// The paths of the images saved to disk, given a cache directory. |
| +base::FilePath FilePathForSessionID(NSString* sessionID, |
| + const base::FilePath& directory) { |
| + base::FilePath path = directory.Append(base::SysNSStringToUTF8(sessionID)) |
| + .ReplaceExtension(".jpg"); |
| + if ([SnapshotCache snapshotScaleForDevice] == 2.0) { |
| + path = path.InsertBeforeExtension("@2x"); |
| + } else if ([SnapshotCache snapshotScaleForDevice] == 3.0) { |
| + path = path.InsertBeforeExtension("@3x"); |
| + } |
| + return path; |
| +} |
| + |
| +base::FilePath GreyFilePathForSessionID(NSString* sessionID, |
| + const base::FilePath& directory) { |
| + base::FilePath path = directory.Append(base::SysNSStringToUTF8(sessionID) + |
| + "Grey").ReplaceExtension(".jpg"); |
| + if ([SnapshotCache snapshotScaleForDevice] == 2.0) { |
| + path = path.InsertBeforeExtension("@2x"); |
| + } else if ([SnapshotCache snapshotScaleForDevice] == 3.0) { |
| + path = path.InsertBeforeExtension("@3x"); |
| + } |
| + return path; |
| +} |
| + |
| +UIImage* ReadImageFromDisk(const base::FilePath& filePath) { |
| + base::ThreadRestrictions::AssertIOAllowed(); |
| + // TODO(justincohen): Consider changing this back to -imageWithContentsOfFile |
| + // instead of -imageWithData, if the crashing rdar://15747161 is ever fixed. |
| + // Tracked in crbug.com/295891. |
| + NSString* path = base::SysUTF8ToNSString(filePath.value()); |
| + return [UIImage imageWithData:[NSData dataWithContentsOfFile:path] |
| + scale:[SnapshotCache snapshotScaleForDevice]]; |
| +} |
| + |
| +void WriteImageToDisk(const base::scoped_nsobject<UIImage>& image, |
| + const base::FilePath& filePath) { |
| + base::ThreadRestrictions::AssertIOAllowed(); |
| + if (!image) |
| + return; |
| + NSString* path = base::SysUTF8ToNSString(filePath.value()); |
| + [UIImageJPEGRepresentation(image, kJPEGImageQuality) writeToFile:path |
| + atomically:YES]; |
| + // Encrypt the snapshot file (mostly for Incognito, but can't hurt to |
| + // always do it). |
| + NSDictionary* attributeDict = |
| + [NSDictionary dictionaryWithObject:NSFileProtectionComplete |
| + forKey:NSFileProtectionKey]; |
| + NSError* error = nil; |
| + BOOL success = [[NSFileManager defaultManager] setAttributes:attributeDict |
| + ofItemAtPath:path |
| + error:&error]; |
| + if (!success) { |
| + DLOG(ERROR) << "Error encrypting thumbnail file" |
| + << base::SysNSStringToUTF8([error description]); |
| + } |
| +} |
| + |
| +void ConvertAndSaveGreyImage( |
| + const base::FilePath& colorPath, |
| + const base::FilePath& greyPath, |
| + const base::scoped_nsobject<UIImage>& cachedImage) { |
| + base::ThreadRestrictions::AssertIOAllowed(); |
| + base::scoped_nsobject<UIImage> colorImage = cachedImage; |
| + if (!colorImage) |
| + colorImage.reset([ReadImageFromDisk(colorPath) retain]); |
| + if (!colorImage) |
| + return; |
| + base::scoped_nsobject<UIImage> greyImage([GreyImage(colorImage) retain]); |
| + WriteImageToDisk(greyImage, greyPath); |
| +} |
| + |
| +} // anonymous namespace |
| + |
| +@interface SnapshotCache () |
| +- (base::FilePath)imagePathForSessionID:(NSString*)sessionID; |
| +- (base::FilePath)greyImagePathForSessionID:(NSString*)sessionID; |
| +// Returns the directory where the thumbnails are saved. |
| +- (base::FilePath)cacheDirectory; |
| +// Returns the directory where the thumbnails were stored in M28 and earlier. |
| +- (base::FilePath)oldCacheDirectory; |
| +// Remove all UIImages from |imageDictionary_|. |
| +- (void)handleEnterBackground; |
| +// Remove all but adjacent UIImages from |imageDictionary_|. |
| +- (void)handleLowMemory; |
| +// Restore adjacent UIImages to |imageDictionary_|. |
| +- (void)handleBecomeActive; |
| +// Clear most recent caller information. |
| +- (void)clearGreySessionInfo; |
| +// Load uncached snapshot image and convert image to grey. |
| +- (void)loadGreyImageAsync:(NSString*)sessionID; |
| +// Save grey image to |greyImageDictionary_| and call into most recent |
| +// |mostRecentGreyBlock_| if |mostRecentGreySessionId_| matches |sessionID|. |
| +- (void)saveGreyImage:(UIImage*)greyImage forKey:(NSString*)sessionID; |
| +@end |
| + |
| +@implementation SnapshotCache |
| + |
| +@synthesize pinnedIDs = pinnedIDs_; |
| + |
| ++ (SnapshotCache*)sharedInstance { |
| + static SnapshotCache* instance = [[SnapshotCache alloc] init]; |
| + return instance; |
| +} |
| + |
| +- (id)init { |
| + if ((self = [super init])) { |
| + propertyReleaser_SnapshotCache_.Init(self, [SnapshotCache class]); |
| + |
| + // TODO(andybons): In the case where the cache grows, it is expensive. |
| + // Make sure this doesn't suck when there are more than ten tabs. |
| + imageDictionary_.reset( |
| + [[NSMutableDictionary alloc] initWithCapacity:kCacheInitialCapacity]); |
| + [[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; |
| +} |
| + |
| ++ (CGFloat)snapshotScaleForDevice { |
| + // 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 (IsIPadIdiom()) { |
| + return 1.0; |
| + } |
| + return [UIScreen mainScreen].scale; |
| +} |
| + |
| +- (void)retrieveImageForSessionID:(NSString*)sessionID |
| + callback:(void (^)(UIImage*))callback { |
| + DCHECK(sessionID); |
| + UIImage* img = [imageDictionary_ objectForKey:sessionID]; |
| + if (img) { |
| + if (callback) |
| + callback(img); |
| + return; |
| + } |
| + |
| + dispatch_queue_t highPriorityQueue = |
| + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); |
| + DCHECK([NSThread isMainThread]); |
| + dispatch_queue_t currentQueue = dispatch_get_main_queue(); |
| + dispatch_async(highPriorityQueue, ^{ |
| + // Retrieve the image on a high priority dispatch queue. |
| + UIImage* image = ReadImageFromDisk([self imagePathForSessionID:sessionID]); |
| + dispatch_async(currentQueue, ^{ |
| + if (image) |
| + [imageDictionary_ setObject:image forKey:sessionID]; |
| + if (callback) |
| + callback(image); |
| + }); |
| + }); |
| +} |
| + |
| +- (void)setImage:(UIImage*)img withSessionID:(NSString*)sessionID { |
| + if (!img || !sessionID) |
| + return; |
| + |
| + // Color snapshots are not used on tablets, so don't keep them in memory. |
| + if (!IsIPadIdiom()) { |
| + [imageDictionary_ setObject:img forKey:sessionID]; |
| + } |
| + // Save the image to disk. |
| + web::WebThread::PostBlockingPoolSequencedTask( |
| + kSequenceToken, FROM_HERE, |
| + base::BindBlock(^{ |
| + base::scoped_nsobject<UIImage> image([img retain]); |
| + WriteImageToDisk(image, [self imagePathForSessionID:sessionID]); |
| + })); |
| +} |
| + |
| +- (void)removeImageWithSessionID:(NSString*)sessionID { |
| + [imageDictionary_ removeObjectForKey:sessionID]; |
| + web::WebThread::PostBlockingPoolSequencedTask( |
| + kSequenceToken, FROM_HERE, |
| + base::BindBlock(^{ |
| + base::FilePath imagePath = [self imagePathForSessionID:sessionID]; |
| + base::DeleteFile(imagePath, false); |
| + base::DeleteFile([self greyImagePathForSessionID:sessionID], false); |
| + })); |
| +} |
| + |
| +- (base::FilePath)oldCacheDirectory { |
| + NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, |
| + NSUserDomainMask, YES); |
| + NSString* path = [paths objectAtIndex:0]; |
| + NSArray* path_components = |
| + [NSArray arrayWithObjects:path, kSnapshotCacheDirectory[1], nil]; |
| + return base::FilePath( |
| + base::SysNSStringToUTF8([NSString pathWithComponents:path_components])); |
| +} |
| + |
| +- (base::FilePath)cacheDirectory { |
| + NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, |
| + NSUserDomainMask, YES); |
| + NSString* path = [paths objectAtIndex:0]; |
| + NSArray* path_components = |
| + [NSArray arrayWithObjects:path, kSnapshotCacheDirectory[0], |
| + kSnapshotCacheDirectory[1], nil]; |
| + return base::FilePath( |
| + base::SysNSStringToUTF8([NSString pathWithComponents:path_components])); |
| +} |
| + |
| +- (base::FilePath)imagePathForSessionID:(NSString*)sessionID { |
| + base::ThreadRestrictions::AssertIOAllowed(); |
| + |
| + base::FilePath path([self cacheDirectory]); |
| + |
| + BOOL exists = base::PathExists(path); |
| + DCHECK(base::DirectoryExists(path) || !exists); |
| + if (!exists) { |
| + bool result = base::CreateDirectory(path); |
| + DCHECK(result); |
| + } |
| + return FilePathForSessionID(sessionID, path); |
| +} |
| + |
| +- (base::FilePath)greyImagePathForSessionID:(NSString*)sessionID { |
| + base::ThreadRestrictions::AssertIOAllowed(); |
| + |
| + base::FilePath path([self cacheDirectory]); |
| + |
| + BOOL exists = base::PathExists(path); |
| + DCHECK(base::DirectoryExists(path) || !exists); |
| + if (!exists) { |
| + bool result = base::CreateDirectory(path); |
| + DCHECK(result); |
| + } |
| + return GreyFilePathForSessionID(sessionID, path); |
| +} |
| + |
| +- (void)purgeCacheOlderThan:(const base::Time&)date |
| + keeping:(NSSet*)liveSessionIds { |
| + // Copying the date, as the block must copy the value, not the reference. |
| + const base::Time dateCopy = date; |
| + web::WebThread::PostBlockingPoolSequencedTask( |
| + kSequenceToken, FROM_HERE, |
| + base::BindBlock(^{ |
| + std::set<base::FilePath> filesToKeep; |
| + for (NSString* sessionID : liveSessionIds) { |
| + base::FilePath curImagePath = [self imagePathForSessionID:sessionID]; |
| + filesToKeep.insert(curImagePath); |
| + filesToKeep.insert([self greyImagePathForSessionID:sessionID]); |
| + } |
| + base::FileEnumerator enumerator([self cacheDirectory], false, |
| + base::FileEnumerator::FILES); |
| + base::FilePath cur_file; |
| + while (!(cur_file = enumerator.Next()).value().empty()) { |
| + if (cur_file.Extension() != ".jpg") |
| + continue; |
| + if (filesToKeep.find(cur_file) != filesToKeep.end()) { |
| + continue; |
| + } |
| + base::FileEnumerator::FileInfo fileInfo = enumerator.GetInfo(); |
| + if (fileInfo.GetLastModifiedTime() > dateCopy) { |
| + continue; |
| + } |
| + base::DeleteFile(cur_file, false); |
| + } |
| + })); |
| +} |
| + |
| +- (void)willBeSavedGreyWhenBackgrounding:(NSString*)sessionID { |
| + if (!sessionID) |
| + return; |
| + backgroundingImageSessionId_.reset([sessionID copy]); |
| + backgroundingColorImage_.reset( |
| + [[imageDictionary_ objectForKey:sessionID] retain]); |
| +} |
| + |
| +- (void)handleLowMemory { |
| + NSMutableDictionary* dictionary = |
| + [[NSMutableDictionary alloc] initWithCapacity:2]; |
| + for (NSString* sessionID in pinnedIDs_) { |
| + UIImage* image = [imageDictionary_ objectForKey:sessionID]; |
| + if (image) |
| + [dictionary setObject:image forKey:sessionID]; |
| + } |
| + imageDictionary_.reset(dictionary); |
| +} |
| + |
| +- (void)handleEnterBackground { |
| + [imageDictionary_ removeAllObjects]; |
| +} |
| + |
| +- (void)handleBecomeActive { |
| + for (NSString* sessionID in pinnedIDs_) |
| + [self retrieveImageForSessionID:sessionID callback:nil]; |
| +} |
| + |
| +- (void)saveGreyImage:(UIImage*)greyImage forKey:(NSString*)sessionID { |
| + if (greyImage) |
| + [greyImageDictionary_ setObject:greyImage forKey:sessionID]; |
| + if ([sessionID isEqualToString:mostRecentGreySessionId_]) { |
| + mostRecentGreyBlock_.get()(greyImage); |
| + [self clearGreySessionInfo]; |
| + } |
| +} |
| + |
| +- (void)loadGreyImageAsync:(NSString*)sessionID { |
| + dispatch_queue_t priorityQueue = |
| + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); |
| + DCHECK([NSThread isMainThread]); |
| + dispatch_queue_t currentQueue = dispatch_get_main_queue(); |
| + // Don't call -retrieveImageForSessionID 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* img = [imageDictionary_ objectForKey:sessionID]; |
| + dispatch_async(priorityQueue, ^{ |
| + // If the image is not in the cache, load it from disk. |
| + UIImage* image = img; |
| + if (!image) |
| + image = ReadImageFromDisk([self imagePathForSessionID:sessionID]); |
| + // If the image is not on disk either, give up. |
| + if (!image) { |
| + dispatch_async(currentQueue, ^{ |
| + [self saveGreyImage:nil forKey:sessionID]; |
| + }); |
| + return; |
| + } |
| + UIImage* greyImage = GreyImage(image); |
| + dispatch_async(currentQueue, ^{ |
| + [self saveGreyImage:greyImage forKey:sessionID]; |
| + }); |
| + }); |
| +} |
| + |
| +- (void)createGreyCache:(NSArray*)sessionIDs { |
| + greyImageDictionary_.reset( |
| + [[NSMutableDictionary alloc] initWithCapacity:kGreyInitialCapacity]); |
| + for (NSString* sessionID in sessionIDs) |
| + [self loadGreyImageAsync:sessionID]; |
| +} |
| + |
| +- (void)removeGreyCache { |
| + greyImageDictionary_.reset(); |
| + [self clearGreySessionInfo]; |
| +} |
| + |
| +- (void)clearGreySessionInfo { |
| + mostRecentGreySessionId_.reset(); |
| + mostRecentGreyBlock_.reset(); |
| +} |
| + |
| +- (void)greyImageForSessionID:(NSString*)sessionID |
| + callback:(void (^)(UIImage*))callback { |
| + DCHECK(greyImageDictionary_); |
| + UIImage* image = [greyImageDictionary_ objectForKey:sessionID]; |
| + if (image) { |
| + callback(image); |
| + [self clearGreySessionInfo]; |
| + } else { |
| + mostRecentGreySessionId_.reset([sessionID copy]); |
| + mostRecentGreyBlock_.reset([callback copy]); |
| + } |
| +} |
| + |
| +- (void)retrieveGreyImageForSessionID:(NSString*)sessionID |
| + callback:(void (^)(UIImage*))callback { |
| + if (greyImageDictionary_) { |
| + UIImage* image = [greyImageDictionary_ objectForKey:sessionID]; |
| + if (image) { |
| + callback(image); |
| + return; |
| + } |
| + } |
| + |
| + dispatch_queue_t highPriorityQueue = |
| + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); |
| + DCHECK([NSThread isMainThread]); |
| + dispatch_queue_t currentQueue = dispatch_get_main_queue(); |
| + dispatch_async(highPriorityQueue, ^{ |
| + // Retrieve the image on a high priority dispatch queue. |
| + // Loading the file into NSData is more reliable. -imageWithContentsOfFile |
| + // would ocassionally claim the image was not a valid jpg. |
| + // "ImageIO: <ERROR> JPEGNot a JPEG file: starts with 0xff 0xd9" |
| + // See http://stackoverflow.com/questions/5081297/ios-uiimagejpegrepresentation-error-not-a-jpeg-file-starts-with-0xff-0xd9 |
| + NSData* imageData = |
| + [NSData dataWithContentsOfFile:base::SysUTF8ToNSString( |
| + [self greyImagePathForSessionID:sessionID].value())]; |
| + UIImage* image = imageData ? [UIImage imageWithData:imageData] : nil; |
| + DCHECK(callback); |
| + dispatch_async(currentQueue, ^{ |
| + if (!image) { |
| + [self retrieveImageForSessionID:sessionID |
| + callback:^(UIImage* image) { |
| + if (callback && image) |
| + callback(GreyImage(image)); |
| + }]; |
| + } else if (callback) { |
| + callback(image); |
| + } |
| + }); |
| + }); |
| +} |
| + |
| +- (void)saveGreyInBackgroundForSessionID:(NSString*)sessionID { |
| + if (!sessionID) |
| + return; |
| + |
| + base::FilePath greyImagePath = |
| + GreyFilePathForSessionID(sessionID, [self cacheDirectory]); |
| + base::FilePath colorImagePath = |
| + FilePathForSessionID(sessionID, [self cacheDirectory]); |
| + |
| + // The color image may still be in memory. Verify the sessionID matches. |
| + if (backgroundingColorImage_) { |
| + if (![backgroundingImageSessionId_ isEqualToString:sessionID]) { |
| + backgroundingColorImage_.reset(); |
| + backgroundingImageSessionId_.reset(); |
| + } |
| + } |
| + |
| + web::WebThread::PostBlockingPoolTask( |
| + FROM_HERE, base::Bind(&ConvertAndSaveGreyImage, colorImagePath, |
| + greyImagePath, backgroundingColorImage_)); |
| +} |
| + |
| +@end |