Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(325)

Unified Diff: ios/chrome/browser/snapshots/snapshot_cache.mm

Issue 862693003: Upstream //ios/chrome/browser/snapshots (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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
« no previous file with comments | « ios/chrome/browser/snapshots/snapshot_cache.h ('k') | ios/chrome/browser/snapshots/snapshot_cache_unittest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698