Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ios / chrome / browser / snapshots / snapshot_cache.mm
blobc33bfe978ff780ca55ba5973e2d08d4b31a10756
1 // Copyright 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "ios/chrome/browser/snapshots/snapshot_cache.h"
7 #import <UIKit/UIKit.h>
9 #include "base/critical_closure.h"
10 #include "base/files/file_enumerator.h"
11 #include "base/files/file_path.h"
12 #include "base/files/file_util.h"
13 #include "base/location.h"
14 #include "base/logging.h"
15 #include "base/mac/bind_objc_block.h"
16 #include "base/mac/scoped_cftyperef.h"
17 #include "base/strings/sys_string_conversions.h"
18 #include "base/task_runner_util.h"
19 #include "base/threading/thread_restrictions.h"
20 #include "ios/chrome/browser/ui/ui_util.h"
21 #import "ios/chrome/browser/ui/uikit_ui_util.h"
22 #include "ios/web/public/web_thread.h"
24 @interface SnapshotCache ()
25 + (base::FilePath)imagePathForSessionID:(NSString*)sessionID;
26 + (base::FilePath)greyImagePathForSessionID:(NSString*)sessionID;
27 // Returns the directory where the thumbnails are saved.
28 + (base::FilePath)cacheDirectory;
29 // Returns the directory where the thumbnails were stored in M28 and earlier.
30 - (base::FilePath)oldCacheDirectory;
31 // Remove all UIImages from |imageDictionary_|.
32 - (void)handleEnterBackground;
33 // Remove all but adjacent UIImages from |imageDictionary_|.
34 - (void)handleLowMemory;
35 // Restore adjacent UIImages to |imageDictionary_|.
36 - (void)handleBecomeActive;
37 // Clear most recent caller information.
38 - (void)clearGreySessionInfo;
39 // Load uncached snapshot image and convert image to grey.
40 - (void)loadGreyImageAsync:(NSString*)sessionID;
41 // Save grey image to |greyImageDictionary_| and call into most recent
42 // |mostRecentGreyBlock_| if |mostRecentGreySessionId_| matches |sessionID|.
43 - (void)saveGreyImage:(UIImage*)greyImage forKey:(NSString*)sessionID;
44 @end
46 namespace {
47 static NSArray* const kSnapshotCacheDirectory = @[ @"Chromium", @"Snapshots" ];
49 const NSUInteger kCacheInitialCapacity = 100;
50 const NSUInteger kGreyInitialCapacity = 8;
51 const CGFloat kJPEGImageQuality = 1.0;  // Highest quality. No compression.
52 // Sequence token to make sure creation/deletion of snapshots don't overlap.
53 const char kSequenceToken[] = "SnapshotCacheSequenceToken";
55 // The paths of the images saved to disk, given a cache directory.
56 base::FilePath FilePathForSessionID(NSString* sessionID,
57                                     const base::FilePath& directory) {
58   base::FilePath path = directory.Append(base::SysNSStringToUTF8(sessionID))
59                             .ReplaceExtension(".jpg");
60   if ([SnapshotCache snapshotScaleForDevice] == 2.0) {
61     path = path.InsertBeforeExtension("@2x");
62   } else if ([SnapshotCache snapshotScaleForDevice] == 3.0) {
63     path = path.InsertBeforeExtension("@3x");
64   }
65   return path;
68 base::FilePath GreyFilePathForSessionID(NSString* sessionID,
69                                         const base::FilePath& directory) {
70   base::FilePath path = directory.Append(base::SysNSStringToUTF8(sessionID) +
71                                          "Grey").ReplaceExtension(".jpg");
72   if ([SnapshotCache snapshotScaleForDevice] == 2.0) {
73     path = path.InsertBeforeExtension("@2x");
74   } else if ([SnapshotCache snapshotScaleForDevice] == 3.0) {
75     path = path.InsertBeforeExtension("@3x");
76   }
77   return path;
80 UIImage* ReadImageFromDisk(const base::FilePath& filePath) {
81   base::ThreadRestrictions::AssertIOAllowed();
82   // TODO(justincohen): Consider changing this back to -imageWithContentsOfFile
83   // instead of -imageWithData, if the crashing rdar://15747161 is ever fixed.
84   // Tracked in crbug.com/295891.
85   NSString* path = base::SysUTF8ToNSString(filePath.value());
86   return [UIImage imageWithData:[NSData dataWithContentsOfFile:path]
87                           scale:[SnapshotCache snapshotScaleForDevice]];
90 void WriteImageToDisk(const base::scoped_nsobject<UIImage>& image,
91                       const base::FilePath& filePath) {
92   base::ThreadRestrictions::AssertIOAllowed();
93   if (!image)
94     return;
95   NSString* path = base::SysUTF8ToNSString(filePath.value());
96   [UIImageJPEGRepresentation(image, kJPEGImageQuality) writeToFile:path
97                                                         atomically:YES];
98   // Encrypt the snapshot file (mostly for Incognito, but can't hurt to
99   // always do it).
100   NSDictionary* attributeDict =
101       [NSDictionary dictionaryWithObject:NSFileProtectionComplete
102                                   forKey:NSFileProtectionKey];
103   NSError* error = nil;
104   BOOL success = [[NSFileManager defaultManager] setAttributes:attributeDict
105                                                   ofItemAtPath:path
106                                                          error:&error];
107   if (!success) {
108     DLOG(ERROR) << "Error encrypting thumbnail file"
109                 << base::SysNSStringToUTF8([error description]);
110   }
113 void ConvertAndSaveGreyImage(
114     const base::FilePath& colorPath,
115     const base::FilePath& greyPath,
116     const base::scoped_nsobject<UIImage>& cachedImage) {
117   base::ThreadRestrictions::AssertIOAllowed();
118   base::scoped_nsobject<UIImage> colorImage = cachedImage;
119   if (!colorImage)
120     colorImage.reset([ReadImageFromDisk(colorPath) retain]);
121   if (!colorImage)
122     return;
123   base::scoped_nsobject<UIImage> greyImage([GreyImage(colorImage) retain]);
124   WriteImageToDisk(greyImage, greyPath);
127 }  // anonymous namespace
129 @implementation SnapshotCache
131 @synthesize pinnedIDs = pinnedIDs_;
133 + (SnapshotCache*)sharedInstance {
134   static SnapshotCache* instance = [[SnapshotCache alloc] init];
135   return instance;
138 - (id)init {
139   if ((self = [super init])) {
140     DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
141     propertyReleaser_SnapshotCache_.Init(self, [SnapshotCache class]);
143     if (!IsIPadIdiom()) {
144       // TODO(jbbegue): In the case where the cache grows, it is expensive.
145       // Make sure this doesn't suck when there are more than ten tabs.
146       imageDictionary_.reset(
147           [[NSMutableDictionary alloc] initWithCapacity:kCacheInitialCapacity]);
148       [[NSNotificationCenter defaultCenter]
149           addObserver:self
150              selector:@selector(handleLowMemory)
151                  name:UIApplicationDidReceiveMemoryWarningNotification
152                object:nil];
153       [[NSNotificationCenter defaultCenter]
154           addObserver:self
155              selector:@selector(handleEnterBackground)
156                  name:UIApplicationDidEnterBackgroundNotification
157                object:nil];
158       [[NSNotificationCenter defaultCenter]
159           addObserver:self
160              selector:@selector(handleBecomeActive)
161                  name:UIApplicationDidBecomeActiveNotification
162                object:nil];
163     }
164   }
165   return self;
168 - (void)dealloc {
169   if (!IsIPadIdiom()) {
170     [[NSNotificationCenter defaultCenter]
171         removeObserver:self
172                   name:UIApplicationDidReceiveMemoryWarningNotification
173                 object:nil];
174     [[NSNotificationCenter defaultCenter]
175         removeObserver:self
176                   name:UIApplicationDidEnterBackgroundNotification
177                 object:nil];
178     [[NSNotificationCenter defaultCenter]
179         removeObserver:self
180                   name:UIApplicationDidBecomeActiveNotification
181                 object:nil];
182   }
183   [super dealloc];
186 + (CGFloat)snapshotScaleForDevice {
187   // On handset, the color snapshot is used for the stack view, so the scale of
188   // the snapshot images should match the scale of the device.
189   // On tablet, the color snapshot is only used to generate the grey snapshot,
190   // which does not have to be high quality, so use scale of 1.0 on all tablets.
191   if (IsIPadIdiom()) {
192     return 1.0;
193   }
194   // Cap snapshot resolution to 2x to reduce the amount of memory they use.
195   return MIN([UIScreen mainScreen].scale, 2.0);
198 - (void)retrieveImageForSessionID:(NSString*)sessionID
199                          callback:(void (^)(UIImage*))callback {
200   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
201   DCHECK(sessionID);
202   // iPad does not cache images, so if there is no callback we can avoid an
203   // expensive read from storage.
204   if (IsIPadIdiom() && !callback)
205     return;
207   UIImage* img = [imageDictionary_ objectForKey:sessionID];
208   if (img) {
209     if (callback)
210       callback(img);
211     return;
212   }
214   base::PostTaskAndReplyWithResult(
215       web::WebThread::GetTaskRunnerForThread(web::WebThread::FILE_USER_BLOCKING)
216           .get(),
217       FROM_HERE, base::BindBlock(^base::scoped_nsobject<UIImage>() {
218         // Retrieve the image on a high priority thread.
219         return base::scoped_nsobject<UIImage>([ReadImageFromDisk(
220             [SnapshotCache imagePathForSessionID:sessionID]) retain]);
221       }),
222       base::BindBlock(^(base::scoped_nsobject<UIImage> image) {
223         // The iPad tab switcher is currently using its own memory cache so the
224         // image is not stored in memory here if running on iPad.
225         // The same logic is used on image writes (code below).
226         if (!IsIPadIdiom() && image)
227           [imageDictionary_ setObject:image forKey:sessionID];
228         if (callback)
229           callback(image);
230       }));
233 - (void)setImage:(UIImage*)img withSessionID:(NSString*)sessionID {
234   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
235   if (!img || !sessionID)
236     return;
238   // The iPad tab switcher is currently using its own memory cache so the image
239   // is not stored in memory here if running on iPad.
240   // The same logic is used on image reads (code above).
241   if (!IsIPadIdiom()) {
242     [imageDictionary_ setObject:img forKey:sessionID];
243   }
244   // Save the image to disk.
245   web::WebThread::PostBlockingPoolSequencedTask(
246       kSequenceToken, FROM_HERE,
247       base::BindBlock(^{
248         base::scoped_nsobject<UIImage> image([img retain]);
249         WriteImageToDisk(image,
250                          [SnapshotCache imagePathForSessionID:sessionID]);
251       }));
254 - (void)removeImageWithSessionID:(NSString*)sessionID {
255   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
256   [imageDictionary_ removeObjectForKey:sessionID];
257   web::WebThread::PostBlockingPoolSequencedTask(
258       kSequenceToken, FROM_HERE,
259       base::BindBlock(^{
260         base::FilePath imagePath =
261             [SnapshotCache imagePathForSessionID:sessionID];
262         base::DeleteFile(imagePath, false);
263         base::DeleteFile([SnapshotCache greyImagePathForSessionID:sessionID],
264                          false);
265       }));
268 - (base::FilePath)oldCacheDirectory {
269   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
270   NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
271                                                        NSUserDomainMask, YES);
272   NSString* path = [paths objectAtIndex:0];
273   NSArray* path_components =
274       [NSArray arrayWithObjects:path, kSnapshotCacheDirectory[1], nil];
275   return base::FilePath(
276       base::SysNSStringToUTF8([NSString pathWithComponents:path_components]));
279 + (base::FilePath)cacheDirectory {
280   NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
281                                                        NSUserDomainMask, YES);
282   NSString* path = [paths objectAtIndex:0];
283   NSArray* path_components =
284       [NSArray arrayWithObjects:path, kSnapshotCacheDirectory[0],
285                                 kSnapshotCacheDirectory[1], nil];
286   return base::FilePath(
287       base::SysNSStringToUTF8([NSString pathWithComponents:path_components]));
290 + (base::FilePath)imagePathForSessionID:(NSString*)sessionID {
291   base::ThreadRestrictions::AssertIOAllowed();
293   base::FilePath path([SnapshotCache cacheDirectory]);
295   BOOL exists = base::PathExists(path);
296   DCHECK(base::DirectoryExists(path) || !exists);
297   if (!exists) {
298     bool result = base::CreateDirectory(path);
299     DCHECK(result);
300   }
301   return FilePathForSessionID(sessionID, path);
304 + (base::FilePath)greyImagePathForSessionID:(NSString*)sessionID {
305   base::ThreadRestrictions::AssertIOAllowed();
307   base::FilePath path([self cacheDirectory]);
309   BOOL exists = base::PathExists(path);
310   DCHECK(base::DirectoryExists(path) || !exists);
311   if (!exists) {
312     bool result = base::CreateDirectory(path);
313     DCHECK(result);
314   }
315   return GreyFilePathForSessionID(sessionID, path);
318 - (void)purgeCacheOlderThan:(const base::Time&)date
319                     keeping:(NSSet*)liveSessionIds {
320   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
321   // Copying the date, as the block must copy the value, not the reference.
322   const base::Time dateCopy = date;
323   web::WebThread::PostBlockingPoolSequencedTask(
324       kSequenceToken, FROM_HERE,
325       base::BindBlock(^{
326         std::set<base::FilePath> filesToKeep;
327         for (NSString* sessionID : liveSessionIds) {
328           base::FilePath curImagePath =
329               [SnapshotCache imagePathForSessionID:sessionID];
330           filesToKeep.insert(curImagePath);
331           filesToKeep.insert(
332               [SnapshotCache greyImagePathForSessionID:sessionID]);
333         }
334         base::FileEnumerator enumerator([SnapshotCache cacheDirectory], false,
335                                         base::FileEnumerator::FILES);
336         base::FilePath cur_file;
337         while (!(cur_file = enumerator.Next()).value().empty()) {
338           if (cur_file.Extension() != ".jpg")
339             continue;
340           if (filesToKeep.find(cur_file) != filesToKeep.end()) {
341             continue;
342           }
343           base::FileEnumerator::FileInfo fileInfo = enumerator.GetInfo();
344           if (fileInfo.GetLastModifiedTime() > dateCopy) {
345             continue;
346           }
347           base::DeleteFile(cur_file, false);
348         }
349       }));
352 - (void)willBeSavedGreyWhenBackgrounding:(NSString*)sessionID {
353   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
354   if (!sessionID)
355     return;
356   backgroundingImageSessionId_.reset([sessionID copy]);
357   backgroundingColorImage_.reset(
358       [[imageDictionary_ objectForKey:sessionID] retain]);
361 - (void)handleLowMemory {
362   DCHECK(!IsIPadIdiom());
363   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
364   NSMutableDictionary* dictionary =
365       [[NSMutableDictionary alloc] initWithCapacity:2];
366   for (NSString* sessionID in pinnedIDs_) {
367     UIImage* image = [imageDictionary_ objectForKey:sessionID];
368     if (image)
369       [dictionary setObject:image forKey:sessionID];
370   }
371   imageDictionary_.reset(dictionary);
374 - (void)handleEnterBackground {
375   DCHECK(!IsIPadIdiom());
376   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
377   [imageDictionary_ removeAllObjects];
380 - (void)handleBecomeActive {
381   DCHECK(!IsIPadIdiom());
382   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
383   for (NSString* sessionID in pinnedIDs_)
384     [self retrieveImageForSessionID:sessionID callback:nil];
387 - (void)saveGreyImage:(UIImage*)greyImage forKey:(NSString*)sessionID {
388   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
389   if (greyImage)
390     [greyImageDictionary_ setObject:greyImage forKey:sessionID];
391   if ([sessionID isEqualToString:mostRecentGreySessionId_]) {
392     mostRecentGreyBlock_.get()(greyImage);
393     [self clearGreySessionInfo];
394   }
397 - (void)loadGreyImageAsync:(NSString*)sessionID {
398   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
399   // Don't call -retrieveImageForSessionID here because it caches the colored
400   // image, which we don't need for the grey image cache. But if the image is
401   // already in the cache, use it.
402   UIImage* img = [imageDictionary_ objectForKey:sessionID];
403   base::PostTaskAndReplyWithResult(
404       web::WebThread::GetTaskRunnerForThread(web::WebThread::FILE_USER_BLOCKING)
405           .get(),
406       FROM_HERE, base::BindBlock(^base::scoped_nsobject<UIImage>() {
407         base::scoped_nsobject<UIImage> result([img retain]);
408         // If the image is not in the cache, load it from disk.
409         if (!result)
410           result.reset([ReadImageFromDisk(
411               [SnapshotCache imagePathForSessionID:sessionID]) retain]);
412         if (result)
413           result.reset([GreyImage(result) retain]);
414         return result;
415       }),
416       base::BindBlock(^(base::scoped_nsobject<UIImage> greyImage) {
417         [self saveGreyImage:greyImage forKey:sessionID];
418       }));
421 - (void)createGreyCache:(NSArray*)sessionIDs {
422   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
423   greyImageDictionary_.reset(
424       [[NSMutableDictionary alloc] initWithCapacity:kGreyInitialCapacity]);
425   for (NSString* sessionID in sessionIDs)
426     [self loadGreyImageAsync:sessionID];
429 - (void)removeGreyCache {
430   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
431   greyImageDictionary_.reset();
432   [self clearGreySessionInfo];
435 - (void)clearGreySessionInfo {
436   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
437   mostRecentGreySessionId_.reset();
438   mostRecentGreyBlock_.reset();
441 - (void)greyImageForSessionID:(NSString*)sessionID
442                      callback:(void (^)(UIImage*))callback {
443   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
444   DCHECK(greyImageDictionary_);
445   UIImage* image = [greyImageDictionary_ objectForKey:sessionID];
446   if (image) {
447     callback(image);
448     [self clearGreySessionInfo];
449   } else {
450     mostRecentGreySessionId_.reset([sessionID copy]);
451     mostRecentGreyBlock_.reset([callback copy]);
452   }
455 - (void)retrieveGreyImageForSessionID:(NSString*)sessionID
456                              callback:(void (^)(UIImage*))callback {
457   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
458   if (greyImageDictionary_) {
459     UIImage* image = [greyImageDictionary_ objectForKey:sessionID];
460     if (image) {
461       callback(image);
462       return;
463     }
464   }
466   base::PostTaskAndReplyWithResult(
467       web::WebThread::GetTaskRunnerForThread(web::WebThread::FILE_USER_BLOCKING)
468           .get(),
469       FROM_HERE, base::BindBlock(^base::scoped_nsobject<UIImage>() {
470         // Retrieve the image on a high priority thread.
471         // Loading the file into NSData is more reliable.
472         // -imageWithContentsOfFile would ocassionally claim the image was not a
473         // valid jpg.
474         // "ImageIO: <ERROR> JPEGNot a JPEG file: starts with 0xff 0xd9"
475         // See
476         // http://stackoverflow.com/questions/5081297/ios-uiimagejpegrepresentation-error-not-a-jpeg-file-starts-with-0xff-0xd9
477         NSData* imageData = [NSData
478             dataWithContentsOfFile:base::SysUTF8ToNSString(
479                 [SnapshotCache greyImagePathForSessionID:sessionID].value())];
480         if (!imageData)
481           return base::scoped_nsobject<UIImage>();
482         DCHECK(callback);
483         return base::scoped_nsobject<UIImage>(
484             [[UIImage imageWithData:imageData] retain]);
485       }),
486       base::BindBlock(^(base::scoped_nsobject<UIImage> image) {
487         if (!image) {
488           [self retrieveImageForSessionID:sessionID
489                                  callback:^(UIImage* img) {
490                                    if (callback && img)
491                                      callback(GreyImage(img));
492                                  }];
493         } else if (callback) {
494           callback(image);
495         }
496       }));
499 - (void)saveGreyInBackgroundForSessionID:(NSString*)sessionID {
500   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
501   if (!sessionID)
502     return;
504   base::FilePath greyImagePath =
505       GreyFilePathForSessionID(sessionID, [SnapshotCache cacheDirectory]);
506   base::FilePath colorImagePath =
507       FilePathForSessionID(sessionID, [SnapshotCache cacheDirectory]);
509   // The color image may still be in memory.  Verify the sessionID matches.
510   if (backgroundingColorImage_) {
511     if (![backgroundingImageSessionId_ isEqualToString:sessionID]) {
512       backgroundingColorImage_.reset();
513       backgroundingImageSessionId_.reset();
514     }
515   }
517   web::WebThread::PostBlockingPoolTask(
518       FROM_HERE, base::Bind(&ConvertAndSaveGreyImage, colorImagePath,
519                             greyImagePath, backgroundingColorImage_));
522 @end