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

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