Add test_runner support for new accessibility event
[chromium-blink-merge.git] / ios / chrome / browser / snapshots / snapshot_cache.mm
blob420a8e1ce436bc596b4170c6604d316f5382bd3c
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/mac/bind_objc_block.h"
11 #include "base/files/file_enumerator.h"
12 #include "base/files/file_path.h"
13 #include "base/files/file_util.h"
14 #include "base/location.h"
15 #include "base/logging.h"
16 #include "base/mac/bind_objc_block.h"
17 #include "base/mac/scoped_cftyperef.h"
18 #include "base/strings/sys_string_conversions.h"
19 #include "base/task_runner_util.h"
20 #include "base/threading/thread_restrictions.h"
21 #include "ios/chrome/browser/ui/ui_util.h"
22 #import "ios/chrome/browser/ui/uikit_ui_util.h"
23 #include "ios/web/public/web_thread.h"
25 @interface SnapshotCache ()
26 + (base::FilePath)imagePathForSessionID:(NSString*)sessionID;
27 + (base::FilePath)greyImagePathForSessionID:(NSString*)sessionID;
28 // Returns the directory where the thumbnails are saved.
29 + (base::FilePath)cacheDirectory;
30 // Returns the directory where the thumbnails were stored in M28 and earlier.
31 - (base::FilePath)oldCacheDirectory;
32 // Remove all UIImages from |imageDictionary_|.
33 - (void)handleEnterBackground;
34 // Remove all but adjacent UIImages from |imageDictionary_|.
35 - (void)handleLowMemory;
36 // Restore adjacent UIImages to |imageDictionary_|.
37 - (void)handleBecomeActive;
38 // Clear most recent caller information.
39 - (void)clearGreySessionInfo;
40 // Load uncached snapshot image and convert image to grey.
41 - (void)loadGreyImageAsync:(NSString*)sessionID;
42 // Save grey image to |greyImageDictionary_| and call into most recent
43 // |mostRecentGreyBlock_| if |mostRecentGreySessionId_| matches |sessionID|.
44 - (void)saveGreyImage:(UIImage*)greyImage forKey:(NSString*)sessionID;
45 @end
47 namespace {
48 static NSArray* const kSnapshotCacheDirectory = @[ @"Chromium", @"Snapshots" ];
50 const NSUInteger kCacheInitialCapacity = 100;
51 const NSUInteger kGreyInitialCapacity = 8;
52 const CGFloat kJPEGImageQuality = 1.0;  // Highest quality. No compression.
53 // Sequence token to make sure creation/deletion of snapshots don't overlap.
54 const char kSequenceToken[] = "SnapshotCacheSequenceToken";
56 // The paths of the images saved to disk, given a cache directory.
57 base::FilePath FilePathForSessionID(NSString* sessionID,
58                                     const base::FilePath& directory) {
59   base::FilePath path = directory.Append(base::SysNSStringToUTF8(sessionID))
60                             .ReplaceExtension(".jpg");
61   if ([SnapshotCache snapshotScaleForDevice] == 2.0) {
62     path = path.InsertBeforeExtension("@2x");
63   } else if ([SnapshotCache snapshotScaleForDevice] == 3.0) {
64     path = path.InsertBeforeExtension("@3x");
65   }
66   return path;
69 base::FilePath GreyFilePathForSessionID(NSString* sessionID,
70                                         const base::FilePath& directory) {
71   base::FilePath path = directory.Append(base::SysNSStringToUTF8(sessionID) +
72                                          "Grey").ReplaceExtension(".jpg");
73   if ([SnapshotCache snapshotScaleForDevice] == 2.0) {
74     path = path.InsertBeforeExtension("@2x");
75   } else if ([SnapshotCache snapshotScaleForDevice] == 3.0) {
76     path = path.InsertBeforeExtension("@3x");
77   }
78   return path;
81 UIImage* ReadImageFromDisk(const base::FilePath& filePath) {
82   base::ThreadRestrictions::AssertIOAllowed();
83   // TODO(justincohen): Consider changing this back to -imageWithContentsOfFile
84   // instead of -imageWithData, if the crashing rdar://15747161 is ever fixed.
85   // Tracked in crbug.com/295891.
86   NSString* path = base::SysUTF8ToNSString(filePath.value());
87   return [UIImage imageWithData:[NSData dataWithContentsOfFile:path]
88                           scale:[SnapshotCache snapshotScaleForDevice]];
91 void WriteImageToDisk(const base::scoped_nsobject<UIImage>& image,
92                       const base::FilePath& filePath) {
93   base::ThreadRestrictions::AssertIOAllowed();
94   if (!image)
95     return;
96   NSString* path = base::SysUTF8ToNSString(filePath.value());
97   [UIImageJPEGRepresentation(image, kJPEGImageQuality) writeToFile:path
98                                                         atomically:YES];
99   // Encrypt the snapshot file (mostly for Incognito, but can't hurt to
100   // always do it).
101   NSDictionary* attributeDict =
102       [NSDictionary dictionaryWithObject:NSFileProtectionComplete
103                                   forKey:NSFileProtectionKey];
104   NSError* error = nil;
105   BOOL success = [[NSFileManager defaultManager] setAttributes:attributeDict
106                                                   ofItemAtPath:path
107                                                          error:&error];
108   if (!success) {
109     DLOG(ERROR) << "Error encrypting thumbnail file"
110                 << base::SysNSStringToUTF8([error description]);
111   }
114 void ConvertAndSaveGreyImage(
115     const base::FilePath& colorPath,
116     const base::FilePath& greyPath,
117     const base::scoped_nsobject<UIImage>& cachedImage) {
118   base::ThreadRestrictions::AssertIOAllowed();
119   base::scoped_nsobject<UIImage> colorImage = cachedImage;
120   if (!colorImage)
121     colorImage.reset([ReadImageFromDisk(colorPath) retain]);
122   if (!colorImage)
123     return;
124   base::scoped_nsobject<UIImage> greyImage([GreyImage(colorImage) retain]);
125   WriteImageToDisk(greyImage, greyPath);
128 }  // anonymous namespace
130 @implementation SnapshotCache
132 @synthesize pinnedIDs = pinnedIDs_;
134 + (SnapshotCache*)sharedInstance {
135   static SnapshotCache* instance = [[SnapshotCache alloc] init];
136   return instance;
139 - (id)init {
140   if ((self = [super init])) {
141     DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
142     propertyReleaser_SnapshotCache_.Init(self, [SnapshotCache class]);
144     // TODO(andybons): 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   return self;
167 + (CGFloat)snapshotScaleForDevice {
168   // On handset, the color snapshot is used for the stack view, so the scale of
169   // the snapshot images should match the scale of the device.
170   // On tablet, the color snapshot is only used to generate the grey snapshot,
171   // which does not have to be high quality, so use scale of 1.0 on all tablets.
172   if (IsIPadIdiom()) {
173     return 1.0;
174   }
175   // Cap snapshot resolution to 2x to reduce the amount of memory they use.
176   return MIN([UIScreen mainScreen].scale, 2.0);
179 - (void)retrieveImageForSessionID:(NSString*)sessionID
180                          callback:(void (^)(UIImage*))callback {
181   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
182   DCHECK(sessionID);
183   UIImage* img = [imageDictionary_ objectForKey:sessionID];
184   if (img) {
185     if (callback)
186       callback(img);
187     return;
188   }
190   base::PostTaskAndReplyWithResult(
191       web::WebThread::GetMessageLoopProxyForThread(
192           web::WebThread::FILE_USER_BLOCKING).get(),
193       FROM_HERE,
194       base::BindBlock(^base::scoped_nsobject<UIImage>() {
195         // Retrieve the image on a high priority thread.
196         return base::scoped_nsobject<UIImage>([ReadImageFromDisk(
197             [SnapshotCache imagePathForSessionID:sessionID]) retain]);
198       }),
199       base::BindBlock(^(base::scoped_nsobject<UIImage> image) {
200         if (image)
201           [imageDictionary_ setObject:image forKey:sessionID];
202         if (callback)
203           callback(image);
204       }));
207 - (void)setImage:(UIImage*)img withSessionID:(NSString*)sessionID {
208   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
209   if (!img || !sessionID)
210     return;
212   // Color snapshots are not used on tablets, so don't keep them in memory.
213   if (!IsIPadIdiom()) {
214     [imageDictionary_ setObject:img forKey:sessionID];
215   }
216   // Save the image to disk.
217   web::WebThread::PostBlockingPoolSequencedTask(
218       kSequenceToken, FROM_HERE,
219       base::BindBlock(^{
220         base::scoped_nsobject<UIImage> image([img retain]);
221         WriteImageToDisk(image,
222                          [SnapshotCache imagePathForSessionID:sessionID]);
223       }));
226 - (void)removeImageWithSessionID:(NSString*)sessionID {
227   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
228   [imageDictionary_ removeObjectForKey:sessionID];
229   web::WebThread::PostBlockingPoolSequencedTask(
230       kSequenceToken, FROM_HERE,
231       base::BindBlock(^{
232         base::FilePath imagePath =
233             [SnapshotCache imagePathForSessionID:sessionID];
234         base::DeleteFile(imagePath, false);
235         base::DeleteFile([SnapshotCache greyImagePathForSessionID:sessionID],
236                          false);
237       }));
240 - (base::FilePath)oldCacheDirectory {
241   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
242   NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
243                                                        NSUserDomainMask, YES);
244   NSString* path = [paths objectAtIndex:0];
245   NSArray* path_components =
246       [NSArray arrayWithObjects:path, kSnapshotCacheDirectory[1], nil];
247   return base::FilePath(
248       base::SysNSStringToUTF8([NSString pathWithComponents:path_components]));
251 + (base::FilePath)cacheDirectory {
252   NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
253                                                        NSUserDomainMask, YES);
254   NSString* path = [paths objectAtIndex:0];
255   NSArray* path_components =
256       [NSArray arrayWithObjects:path, kSnapshotCacheDirectory[0],
257                                 kSnapshotCacheDirectory[1], nil];
258   return base::FilePath(
259       base::SysNSStringToUTF8([NSString pathWithComponents:path_components]));
262 + (base::FilePath)imagePathForSessionID:(NSString*)sessionID {
263   base::ThreadRestrictions::AssertIOAllowed();
265   base::FilePath path([SnapshotCache cacheDirectory]);
267   BOOL exists = base::PathExists(path);
268   DCHECK(base::DirectoryExists(path) || !exists);
269   if (!exists) {
270     bool result = base::CreateDirectory(path);
271     DCHECK(result);
272   }
273   return FilePathForSessionID(sessionID, path);
276 + (base::FilePath)greyImagePathForSessionID:(NSString*)sessionID {
277   base::ThreadRestrictions::AssertIOAllowed();
279   base::FilePath path([self cacheDirectory]);
281   BOOL exists = base::PathExists(path);
282   DCHECK(base::DirectoryExists(path) || !exists);
283   if (!exists) {
284     bool result = base::CreateDirectory(path);
285     DCHECK(result);
286   }
287   return GreyFilePathForSessionID(sessionID, path);
290 - (void)purgeCacheOlderThan:(const base::Time&)date
291                     keeping:(NSSet*)liveSessionIds {
292   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
293   // Copying the date, as the block must copy the value, not the reference.
294   const base::Time dateCopy = date;
295   web::WebThread::PostBlockingPoolSequencedTask(
296       kSequenceToken, FROM_HERE,
297       base::BindBlock(^{
298         std::set<base::FilePath> filesToKeep;
299         for (NSString* sessionID : liveSessionIds) {
300           base::FilePath curImagePath =
301               [SnapshotCache imagePathForSessionID:sessionID];
302           filesToKeep.insert(curImagePath);
303           filesToKeep.insert(
304               [SnapshotCache greyImagePathForSessionID:sessionID]);
305         }
306         base::FileEnumerator enumerator([SnapshotCache cacheDirectory], false,
307                                         base::FileEnumerator::FILES);
308         base::FilePath cur_file;
309         while (!(cur_file = enumerator.Next()).value().empty()) {
310           if (cur_file.Extension() != ".jpg")
311             continue;
312           if (filesToKeep.find(cur_file) != filesToKeep.end()) {
313             continue;
314           }
315           base::FileEnumerator::FileInfo fileInfo = enumerator.GetInfo();
316           if (fileInfo.GetLastModifiedTime() > dateCopy) {
317             continue;
318           }
319           base::DeleteFile(cur_file, false);
320         }
321       }));
324 - (void)willBeSavedGreyWhenBackgrounding:(NSString*)sessionID {
325   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
326   if (!sessionID)
327     return;
328   backgroundingImageSessionId_.reset([sessionID copy]);
329   backgroundingColorImage_.reset(
330       [[imageDictionary_ objectForKey:sessionID] retain]);
333 - (void)handleLowMemory {
334   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
335   NSMutableDictionary* dictionary =
336       [[NSMutableDictionary alloc] initWithCapacity:2];
337   for (NSString* sessionID in pinnedIDs_) {
338     UIImage* image = [imageDictionary_ objectForKey:sessionID];
339     if (image)
340       [dictionary setObject:image forKey:sessionID];
341   }
342   imageDictionary_.reset(dictionary);
345 - (void)handleEnterBackground {
346   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
347   [imageDictionary_ removeAllObjects];
350 - (void)handleBecomeActive {
351   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
352   for (NSString* sessionID in pinnedIDs_)
353     [self retrieveImageForSessionID:sessionID callback:nil];
356 - (void)saveGreyImage:(UIImage*)greyImage forKey:(NSString*)sessionID {
357   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
358   if (greyImage)
359     [greyImageDictionary_ setObject:greyImage forKey:sessionID];
360   if ([sessionID isEqualToString:mostRecentGreySessionId_]) {
361     mostRecentGreyBlock_.get()(greyImage);
362     [self clearGreySessionInfo];
363   }
366 - (void)loadGreyImageAsync:(NSString*)sessionID {
367   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
368   // Don't call -retrieveImageForSessionID here because it caches the colored
369   // image, which we don't need for the grey image cache. But if the image is
370   // already in the cache, use it.
371   UIImage* img = [imageDictionary_ objectForKey:sessionID];
372   base::PostTaskAndReplyWithResult(
373       web::WebThread::GetMessageLoopProxyForThread(
374           web::WebThread::FILE_USER_BLOCKING).get(),
375       FROM_HERE,
376       base::BindBlock(^base::scoped_nsobject<UIImage>() {
377         base::scoped_nsobject<UIImage> result([img retain]);
378         // If the image is not in the cache, load it from disk.
379         if (!result)
380           result.reset([ReadImageFromDisk(
381               [SnapshotCache imagePathForSessionID:sessionID]) retain]);
382         if (result)
383           result.reset([GreyImage(result) retain]);
384         return result;
385       }),
386       base::BindBlock(^(base::scoped_nsobject<UIImage> greyImage) {
387         [self saveGreyImage:greyImage forKey:sessionID];
388       }));
391 - (void)createGreyCache:(NSArray*)sessionIDs {
392   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
393   greyImageDictionary_.reset(
394       [[NSMutableDictionary alloc] initWithCapacity:kGreyInitialCapacity]);
395   for (NSString* sessionID in sessionIDs)
396     [self loadGreyImageAsync:sessionID];
399 - (void)removeGreyCache {
400   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
401   greyImageDictionary_.reset();
402   [self clearGreySessionInfo];
405 - (void)clearGreySessionInfo {
406   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
407   mostRecentGreySessionId_.reset();
408   mostRecentGreyBlock_.reset();
411 - (void)greyImageForSessionID:(NSString*)sessionID
412                      callback:(void (^)(UIImage*))callback {
413   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
414   DCHECK(greyImageDictionary_);
415   UIImage* image = [greyImageDictionary_ objectForKey:sessionID];
416   if (image) {
417     callback(image);
418     [self clearGreySessionInfo];
419   } else {
420     mostRecentGreySessionId_.reset([sessionID copy]);
421     mostRecentGreyBlock_.reset([callback copy]);
422   }
425 - (void)retrieveGreyImageForSessionID:(NSString*)sessionID
426                              callback:(void (^)(UIImage*))callback {
427   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
428   if (greyImageDictionary_) {
429     UIImage* image = [greyImageDictionary_ objectForKey:sessionID];
430     if (image) {
431       callback(image);
432       return;
433     }
434   }
436   base::PostTaskAndReplyWithResult(
437       web::WebThread::GetMessageLoopProxyForThread(
438           web::WebThread::FILE_USER_BLOCKING).get(),
439       FROM_HERE,
440       base::BindBlock(^base::scoped_nsobject<UIImage>() {
441         // Retrieve the image on a high priority thread.
442         // Loading the file into NSData is more reliable.
443         // -imageWithContentsOfFile would ocassionally claim the image was not a
444         // valid jpg.
445         // "ImageIO: <ERROR> JPEGNot a JPEG file: starts with 0xff 0xd9"
446         // See
447         // http://stackoverflow.com/questions/5081297/ios-uiimagejpegrepresentation-error-not-a-jpeg-file-starts-with-0xff-0xd9
448         NSData* imageData = [NSData
449             dataWithContentsOfFile:base::SysUTF8ToNSString(
450                 [SnapshotCache greyImagePathForSessionID:sessionID].value())];
451         if (!imageData)
452           return base::scoped_nsobject<UIImage>();
453         DCHECK(callback);
454         return base::scoped_nsobject<UIImage>(
455             [[UIImage imageWithData:imageData] retain]);
456       }),
457       base::BindBlock(^(base::scoped_nsobject<UIImage> image) {
458         if (!image) {
459           [self retrieveImageForSessionID:sessionID
460                                  callback:^(UIImage* img) {
461                                    if (callback && img)
462                                      callback(GreyImage(img));
463                                  }];
464         } else if (callback) {
465           callback(image);
466         }
467       }));
470 - (void)saveGreyInBackgroundForSessionID:(NSString*)sessionID {
471   DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
472   if (!sessionID)
473     return;
475   base::FilePath greyImagePath =
476       GreyFilePathForSessionID(sessionID, [SnapshotCache cacheDirectory]);
477   base::FilePath colorImagePath =
478       FilePathForSessionID(sessionID, [SnapshotCache cacheDirectory]);
480   // The color image may still be in memory.  Verify the sessionID matches.
481   if (backgroundingColorImage_) {
482     if (![backgroundingImageSessionId_ isEqualToString:sessionID]) {
483       backgroundingColorImage_.reset();
484       backgroundingImageSessionId_.reset();
485     }
486   }
488   web::WebThread::PostBlockingPoolTask(
489       FROM_HERE, base::Bind(&ConvertAndSaveGreyImage, colorImagePath,
490                             greyImagePath, backgroundingColorImage_));
493 @end