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

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

Powered by Google App Engine
This is Rietveld 408576698