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..5b8c9a10ae4d37d266a22dc7edcfa67fd71d4633 |
--- /dev/null |
+++ b/ios/chrome/browser/snapshots/snapshot_cache.mm |
@@ -0,0 +1,494 @@ |
+// Copyright 2012 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/browser/snapshots/snapshot_cache.h" |
+ |
+#import <UIKit/UIKit.h> |
+ |
+#include "base/critical_closure.h" |
+#include "base/mac/bind_objc_block.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/task_runner_util.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" |
+ |
+@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 |
+ |
+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 |
+ |
+@implementation SnapshotCache |
+ |
+@synthesize pinnedIDs = pinnedIDs_; |
+ |
++ (SnapshotCache*)sharedInstance { |
+ static SnapshotCache* instance = [[SnapshotCache alloc] init]; |
+ return instance; |
+} |
+ |
+- (id)init { |
+ if ((self = [super init])) { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ 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; |
+ } |
+ // Cap snapshot resolution to 2x to reduce the amount of memory they use. |
+ return MIN([UIScreen mainScreen].scale, 2.0); |
+} |
+ |
+- (void)retrieveImageForSessionID:(NSString*)sessionID |
+ callback:(void (^)(UIImage*))callback { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ DCHECK(sessionID); |
+ UIImage* img = [imageDictionary_ objectForKey:sessionID]; |
+ if (img) { |
+ if (callback) |
+ callback(img); |
+ return; |
+ } |
+ |
+ base::PostTaskAndReplyWithResult( |
+ web::WebThread::GetMessageLoopProxyForThread( |
+ web::WebThread::FILE_USER_BLOCKING).get(), |
+ FROM_HERE, |
+ base::BindBlock(^base::scoped_nsobject<UIImage>() { |
+ // Retrieve the image on a high priority thread. |
+ return base::scoped_nsobject<UIImage>([ReadImageFromDisk( |
+ [SnapshotCache imagePathForSessionID:sessionID]) retain]); |
+ }), |
+ base::BindBlock(^(base::scoped_nsobject<UIImage> image) { |
+ if (image) |
+ [imageDictionary_ setObject:image forKey:sessionID]; |
+ if (callback) |
+ callback(image); |
+ })); |
+} |
+ |
+- (void)setImage:(UIImage*)img withSessionID:(NSString*)sessionID { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ 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, |
+ [SnapshotCache imagePathForSessionID:sessionID]); |
+ })); |
+} |
+ |
+- (void)removeImageWithSessionID:(NSString*)sessionID { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ [imageDictionary_ removeObjectForKey:sessionID]; |
+ web::WebThread::PostBlockingPoolSequencedTask( |
+ kSequenceToken, FROM_HERE, |
+ base::BindBlock(^{ |
+ base::FilePath imagePath = |
+ [SnapshotCache imagePathForSessionID:sessionID]; |
+ base::DeleteFile(imagePath, false); |
+ base::DeleteFile([SnapshotCache greyImagePathForSessionID:sessionID], |
+ false); |
+ })); |
+} |
+ |
+- (base::FilePath)oldCacheDirectory { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ 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([SnapshotCache 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 { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ // 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 = |
+ [SnapshotCache imagePathForSessionID:sessionID]; |
+ filesToKeep.insert(curImagePath); |
+ filesToKeep.insert( |
+ [SnapshotCache greyImagePathForSessionID:sessionID]); |
+ } |
+ base::FileEnumerator enumerator([SnapshotCache 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 { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ if (!sessionID) |
+ return; |
+ backgroundingImageSessionId_.reset([sessionID copy]); |
+ backgroundingColorImage_.reset( |
+ [[imageDictionary_ objectForKey:sessionID] retain]); |
+} |
+ |
+- (void)handleLowMemory { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ 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 { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ [imageDictionary_ removeAllObjects]; |
+} |
+ |
+- (void)handleBecomeActive { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ for (NSString* sessionID in pinnedIDs_) |
+ [self retrieveImageForSessionID:sessionID callback:nil]; |
+} |
+ |
+- (void)saveGreyImage:(UIImage*)greyImage forKey:(NSString*)sessionID { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ if (greyImage) |
+ [greyImageDictionary_ setObject:greyImage forKey:sessionID]; |
+ if ([sessionID isEqualToString:mostRecentGreySessionId_]) { |
+ mostRecentGreyBlock_.get()(greyImage); |
+ [self clearGreySessionInfo]; |
+ } |
+} |
+ |
+- (void)loadGreyImageAsync:(NSString*)sessionID { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ // 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]; |
+ base::PostTaskAndReplyWithResult( |
+ web::WebThread::GetMessageLoopProxyForThread( |
+ web::WebThread::FILE_USER_BLOCKING).get(), |
+ FROM_HERE, |
+ base::BindBlock(^base::scoped_nsobject<UIImage>() { |
+ base::scoped_nsobject<UIImage> result([img retain]); |
+ // If the image is not in the cache, load it from disk. |
+ if (!result) |
+ result.reset([ReadImageFromDisk( |
+ [SnapshotCache imagePathForSessionID:sessionID]) retain]); |
+ if (result) |
+ result.reset([GreyImage(result) retain]); |
+ return result; |
+ }), |
+ base::BindBlock(^(base::scoped_nsobject<UIImage> greyImage) { |
+ [self saveGreyImage:greyImage forKey:sessionID]; |
+ })); |
+} |
+ |
+- (void)createGreyCache:(NSArray*)sessionIDs { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ greyImageDictionary_.reset( |
+ [[NSMutableDictionary alloc] initWithCapacity:kGreyInitialCapacity]); |
+ for (NSString* sessionID in sessionIDs) |
+ [self loadGreyImageAsync:sessionID]; |
+} |
+ |
+- (void)removeGreyCache { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ greyImageDictionary_.reset(); |
+ [self clearGreySessionInfo]; |
+} |
+ |
+- (void)clearGreySessionInfo { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ mostRecentGreySessionId_.reset(); |
+ mostRecentGreyBlock_.reset(); |
+} |
+ |
+- (void)greyImageForSessionID:(NSString*)sessionID |
+ callback:(void (^)(UIImage*))callback { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ 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 { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ if (greyImageDictionary_) { |
+ UIImage* image = [greyImageDictionary_ objectForKey:sessionID]; |
+ if (image) { |
+ callback(image); |
+ return; |
+ } |
+ } |
+ |
+ base::PostTaskAndReplyWithResult( |
+ web::WebThread::GetMessageLoopProxyForThread( |
+ web::WebThread::FILE_USER_BLOCKING).get(), |
+ FROM_HERE, |
+ base::BindBlock(^base::scoped_nsobject<UIImage>() { |
+ // Retrieve the image on a high priority thread. |
+ // 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( |
+ [SnapshotCache greyImagePathForSessionID:sessionID].value())]; |
+ if (!imageData) |
+ return base::scoped_nsobject<UIImage>(); |
+ DCHECK(callback); |
+ return base::scoped_nsobject<UIImage>( |
+ [[UIImage imageWithData:imageData] retain]); |
+ }), |
+ base::BindBlock(^(base::scoped_nsobject<UIImage> image) { |
+ if (!image) { |
+ [self retrieveImageForSessionID:sessionID |
+ callback:^(UIImage* img) { |
+ if (callback && img) |
+ callback(GreyImage(img)); |
+ }]; |
+ } else if (callback) { |
+ callback(image); |
+ } |
+ })); |
+} |
+ |
+- (void)saveGreyInBackgroundForSessionID:(NSString*)sessionID { |
+ DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); |
+ if (!sessionID) |
+ return; |
+ |
+ base::FilePath greyImagePath = |
+ GreyFilePathForSessionID(sessionID, [SnapshotCache cacheDirectory]); |
+ base::FilePath colorImagePath = |
+ FilePathForSessionID(sessionID, [SnapshotCache 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 |