chromium/ios/chrome/browser/snapshots/model/legacy_snapshot_storage.mm

// Copyright 2012 The Chromium Authors
// 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/model/legacy_snapshot_storage.h"

#import <map>

#import "base/containers/contains.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/functional/bind.h"
#import "base/functional/callback_forward.h"
#import "base/ios/crb_protocol_observers.h"
#import "base/logging.h"
#import "base/metrics/histogram_functions.h"
#import "base/time/time.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/snapshots/model/features.h"
#import "ios/chrome/browser/snapshots/model/legacy_image_file_manager.h"
#import "ios/chrome/browser/snapshots/model/legacy_snapshot_lru_cache.h"
#import "ios/chrome/browser/snapshots/model/legacy_snapshot_storage+Testing.h"
#import "ios/chrome/browser/snapshots/model/model_swift.h"
#import "ios/chrome/browser/snapshots/model/snapshot_id.h"
#import "ios/chrome/browser/snapshots/model/snapshot_id_wrapper.h"

namespace {

// Maximum size in number of elements that the LRU cache can hold before
// starting to evict elements.
const NSUInteger kLRUCacheMaxCapacity = 6;

// Maximum size in number of elements that the LRU cache can hold before
// starting to evict elements when PinnedTabs feature is enabled.
//
// To calculate the cache size number we'll start with the assumption that
// currently snapshot preloading feature "works fine". In the reality it might
// not be the case for large screen devices such as iPad. Another assumption
// here is that pinned tabs feature requires on average 4 more snapshots to be
// used. Based on that kLRUCacheMaxCapacityForPinnedTabsEnabled is
// kLRUCacheMaxCapacity which "works fine" + on average 4 more snapshots needed
// for pinned tabs feature.
const NSUInteger kLRUCacheMaxCapacityForPinnedTabsEnabled = 10;

}  // namespace

// Protocol observers subclass that explicitly implements
// <SnapshotStorageObserver>.
@interface SnapshotStorageObservers
    : CRBProtocolObservers <SnapshotStorageObserver>
+ (instancetype)observers;
@end

@implementation SnapshotStorageObservers
+ (instancetype)observers {
  return [self observersWithProtocol:@protocol(SnapshotStorageObserver)];
}
@end

@interface LegacySnapshotStorage ()
// List of observers to be notified of changes to the snapshot storage.
@property(nonatomic, strong) SnapshotStorageObservers* observers;
@end

@implementation LegacySnapshotStorage {
  // Cache to hold color snapshots in memory. n.b. Color snapshots are not
  // kept in memory on tablets.
  LegacySnapshotLRUCache<UIImage*>* _lruCache;

  // File manager to read/write images from/to disk.
  __strong LegacyImageFileManager* _fileManager;
}

- (instancetype)initWithStoragePath:(const base::FilePath&)storagePath
                         legacyPath:(const base::FilePath&)legacyPath {
  if ((self = [super init])) {
    NSUInteger cacheSize = IsPinnedTabsEnabled()
                               ? kLRUCacheMaxCapacityForPinnedTabsEnabled
                               : kLRUCacheMaxCapacity;
    _lruCache = [[LegacySnapshotLRUCache alloc] initWithCacheSize:cacheSize];

    _fileManager =
        [[LegacyImageFileManager alloc] initWithStoragePath:storagePath
                                                 legacyPath:legacyPath];

    _observers = [SnapshotStorageObservers observers];

    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(handleLowMemory)
               name:UIApplicationDidReceiveMemoryWarningNotification
             object:nil];
    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(handleEnterBackground)
               name:UIApplicationDidEnterBackgroundNotification
             object:nil];
  }
  return self;
}

- (instancetype)initWithStoragePath:(const base::FilePath&)storagePath {
  return [self initWithStoragePath:storagePath legacyPath:base::FilePath()];
}

- (void)dealloc {
  [[NSNotificationCenter defaultCenter]
      removeObserver:self
                name:UIApplicationDidReceiveMemoryWarningNotification
              object:nil];
  [[NSNotificationCenter defaultCenter]
      removeObserver:self
                name:UIApplicationDidEnterBackgroundNotification
              object:nil];
}

- (void)retrieveImageForSnapshotID:(SnapshotID)snapshotID
                          callback:(void (^)(UIImage*))callback {
  DCHECK(snapshotID.valid());
  DCHECK(callback);

  if (UIImage* image = [_lruCache objectForKey:snapshotID]) {
    callback(image);
    return;
  }

  __weak LegacySnapshotLRUCache* weakLRUCache = _lruCache;
  [_fileManager readImageWithSnapshotID:snapshotID
                             completion:base::BindOnce(^(UIImage* image) {
                               if (image) {
                                 [weakLRUCache setObject:image
                                                  forKey:snapshotID];
                               }
                               callback(image);
                             })];
}

- (void)retrieveGreyImageForSnapshotID:(SnapshotID)snapshotID
                              callback:(void (^)(UIImage*))callback {
  DCHECK(snapshotID.valid());
  DCHECK(callback);

  // There are no grey images stored in disk, so generate it from a colored
  // snapshot if exists.
  UIImage* colorImage = [_lruCache objectForKey:snapshotID];
  if (colorImage) {
    callback(GreyImage(colorImage));
    return;
  }

  // Fallback to reading a color image from disk when there is no color image in
  // LRU cache.
  [_fileManager readImageWithSnapshotID:snapshotID
                             completion:base::BindOnce(^(UIImage* image) {
                               if (image) {
                                 callback(GreyImage(image));
                                 return;
                               }
                               callback(nil);
                             })];
}

- (void)setImage:(UIImage*)image withSnapshotID:(SnapshotID)snapshotID {
  if (!image || !snapshotID.valid()) {
    return;
  }

  [_lruCache setObject:image forKey:snapshotID];

  // Each image in the cache has the same resolution and hence the same size.
  size_t imageSizes = CGImageGetBytesPerRow(image.CGImage) *
                      CGImageGetHeight(image.CGImage) * [_lruCache count];
  base::UmaHistogramMemoryKB("IOS.Snapshots.CacheSize", imageSizes / 1024);

  [self.observers
      didUpdateSnapshotStorageWithSnapshotID:
          [[SnapshotIDWrapper alloc] initWithSnapshotID:snapshotID]];

  // Save the image to disk.
  [_fileManager writeImage:image withSnapshotID:snapshotID];
}

- (void)removeImageWithSnapshotID:(SnapshotID)snapshotID {
  [_lruCache removeObjectForKey:snapshotID];

  [self.observers
      didUpdateSnapshotStorageWithSnapshotID:
          [[SnapshotIDWrapper alloc] initWithSnapshotID:snapshotID]];

  [_fileManager removeImageWithSnapshotID:snapshotID];
}

- (void)removeAllImages {
  [_lruCache removeAllObjects];

  [_fileManager removeAllImages];
}

- (void)purgeImagesOlderThan:(base::Time)date
                     keeping:(const std::vector<SnapshotID>&)liveSnapshotIDs {
  [_fileManager purgeImagesOlderThan:date keeping:liveSnapshotIDs];
}

- (void)renameSnapshotsWithIDs:(NSArray<NSString*>*)oldIDs
                         toIDs:(const std::vector<SnapshotID>&)newIDs {
  DCHECK_EQ(oldIDs.count, newIDs.size());
  [_fileManager renameSnapshotsWithIDs:oldIDs toIDs:newIDs];
}

- (void)migrateImageWithSnapshotID:(SnapshotID)snapshotID
                 toSnapshotStorage:(LegacySnapshotStorage*)destinationStorage {
  // Copy to the destination storage.
  if (UIImage* image = [_lruCache objectForKey:snapshotID]) {
    // Copy both on-disk and in-memory versions.
    [destinationStorage setImage:image withSnapshotID:snapshotID];
  } else {
    // Only copy on-disk.
    [_fileManager
        copyImage:[self imagePathForSnapshotID:snapshotID]
        toNewPath:[destinationStorage imagePathForSnapshotID:snapshotID]];
  }

  // Remove the snapshot from this storage.
  [self removeImageWithSnapshotID:snapshotID];
}

// Remove all UIImages from `lruCache_`.
- (void)handleLowMemory {
  [_lruCache removeAllObjects];
}

// Remove all UIImages from `lruCache_`.
- (void)handleEnterBackground {
  [_lruCache removeAllObjects];
}

- (void)addObserver:(id<SnapshotStorageObserver>)observer {
  [self.observers addObserver:observer];
}

- (void)removeObserver:(id<SnapshotStorageObserver>)observer {
  [self.observers removeObserver:observer];
}

- (void)shutdown {
  [_fileManager shutdown];
}

#pragma mark - Testing

- (base::FilePath)imagePathForSnapshotID:(SnapshotID)snapshotID {
  return [_fileManager imagePathForSnapshotID:snapshotID];
}

- (NSUInteger)lruCacheMaxSize {
  return [_lruCache maxCacheSize];
}

- (void)clearCache {
  [_lruCache removeAllObjects];
}

@end