chromium/ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_tile_saver.mm

// Copyright 2022 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/ui/content_suggestions/cells/content_suggestions_tile_saver.h"

#import "base/functional/bind.h"
#import "base/hash/md5.h"
#import "base/logging.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/thread_pool.h"
#import "base/threading/scoped_blocking_call.h"
#import "components/favicon/core/fallback_url_util.h"
#import "components/ntp_tiles/ntp_tile.h"
#import "ios/chrome/browser/favicon/ui_bundled/favicon_attributes_provider.h"
#import "ios/chrome/browser/widget_kit/model/features.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "ios/chrome/common/ntp_tile/ntp_tile.h"
#import "ios/chrome/common/ui/favicon/favicon_attributes.h"
#import "net/base/apple/url_conversions.h"

#if BUILDFLAG(ENABLE_WIDGET_KIT_EXTENSION)
#import "ios/chrome/browser/widget_kit/model/model_swift.h"
#endif

namespace content_suggestions_tile_saver {

// Write the `most_visited_sites` to disk.
void WriteSavedMostVisited(NSDictionary<NSURL*, NTPTile*>* most_visited_sites);

// Checks if every site in `tiles` has had its favicons fetched. If so, writes
// the info to disk, saving the favicons to `favicons_directory`.
void WriteToDiskIfComplete(NSDictionary<NSURL*, NTPTile*>* tiles,
                           NSURL* favicons_directory);

// Gets a name for the favicon file.
NSString* GetFaviconFileName(const GURL& url);

// If the sites currently saved include one with `tile`'s url, replace it by
// `tile`.
void WriteSingleUpdatedTileToDisk(NTPTile* tile);

// Get the favicons using `favicon_provider` and writes them to disk.
void GetFaviconsAndSave(const ntp_tiles::NTPTilesVector& most_visited_data,
                        FaviconAttributesProvider* favicon_provider,
                        NSURL* favicons_directory);

// Updates the list of tiles that must be displayed in the content suggestion
// widget.
void UpdateTileList(const ntp_tiles::NTPTilesVector& most_visited_data);

// Deletes icons contained in `favicons_directory` and corresponding to no URL
// in `most_visited_data`.
void ClearOutdatedIcons(const ntp_tiles::NTPTilesVector& most_visited_data,
                        NSURL* favicons_directory);

}  // namespace content_suggestions_tile_saver

namespace content_suggestions_tile_saver {

void UpdateTileList(const ntp_tiles::NTPTilesVector& most_visited_data) {
  NSMutableDictionary<NSURL*, NTPTile*>* tiles =
      [[NSMutableDictionary alloc] init];
  NSDictionary<NSURL*, NTPTile*>* old_tiles = ReadSavedMostVisited();
  for (size_t i = 0; i < most_visited_data.size(); i++) {
    const ntp_tiles::NTPTile& ntp_tile = most_visited_data[i];
    NSURL* ns_url = net::NSURLWithGURL(ntp_tile.url);
    if (!ns_url) {
      // If the URL for a particular tile is invalid, skip including it.
      continue;
    }
    NTPTile* tile =
        [[NTPTile alloc] initWithTitle:base::SysUTF16ToNSString(ntp_tile.title)
                                   URL:ns_url
                              position:i];
    tile.faviconFileName = GetFaviconFileName(ntp_tile.url);
    NTPTile* old_tile = [old_tiles objectForKey:ns_url];
    if (old_tile) {
      // Keep fallback data.
      tile.fallbackMonogram = old_tile.fallbackMonogram;
      tile.fallbackTextColor = old_tile.fallbackTextColor;
      tile.fallbackIsDefaultColor = old_tile.fallbackIsDefaultColor;
      tile.fallbackBackgroundColor = old_tile.fallbackBackgroundColor;
    }
    [tiles setObject:tile forKey:tile.URL];
  }
  WriteSavedMostVisited(tiles);
}

NSString* GetFaviconFileName(const GURL& url) {
  return [base::SysUTF8ToNSString(base::MD5String(url.spec()))
      stringByAppendingString:@".png"];
}

void GetFaviconsAndSave(const ntp_tiles::NTPTilesVector& most_visited_data,
                        FaviconAttributesProvider* favicon_provider,
                        NSURL* favicons_directory) {
  for (size_t i = 0; i < most_visited_data.size(); i++) {
    const GURL& gurl = most_visited_data[i].url;
    UpdateSingleFavicon(gurl, favicon_provider, favicons_directory);
  }
}

void ClearOutdatedIcons(const ntp_tiles::NTPTilesVector& most_visited_data,
                        NSURL* favicons_directory) {
  NSMutableSet<NSString*>* allowed_files_name = [[NSMutableSet alloc] init];
  for (size_t i = 0; i < most_visited_data.size(); i++) {
    const ntp_tiles::NTPTile& ntp_tile = most_visited_data[i];
    NSString* favicon_file_name = GetFaviconFileName(ntp_tile.url);
    [allowed_files_name addObject:favicon_file_name];
  }
  [[NSFileManager defaultManager] createDirectoryAtURL:favicons_directory
                           withIntermediateDirectories:YES
                                            attributes:nil
                                                 error:nil];
  NSArray<NSURL*>* existing_files = [[NSFileManager defaultManager]
        contentsOfDirectoryAtURL:favicons_directory
      includingPropertiesForKeys:nil
                         options:0
                           error:nil];
  for (NSURL* file : existing_files) {
    if (![allowed_files_name containsObject:[file lastPathComponent]]) {
      [[NSFileManager defaultManager] removeItemAtURL:file error:nil];
    }
  }
}

void SaveMostVisitedToDisk(const ntp_tiles::NTPTilesVector& most_visited_data,
                           FaviconAttributesProvider* favicon_provider,
                           NSURL* favicons_directory) {
  if (favicons_directory == nil) {
    return;
  }
  UpdateTileList(most_visited_data);

  base::ThreadPool::PostTaskAndReply(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&ClearOutdatedIcons, most_visited_data,
                     favicons_directory),
      base::BindOnce(
          ^(const ntp_tiles::NTPTilesVector& inner_most_visited_data) {
            GetFaviconsAndSave(inner_most_visited_data, favicon_provider,
                               favicons_directory);
          },
          most_visited_data));
}

void WriteSingleUpdatedTileToDisk(NTPTile* tile) {
  NSMutableDictionary* tiles = [ReadSavedMostVisited() mutableCopy];
  if (![tiles objectForKey:tile.URL]) {
    return;
  }
  [tiles setObject:tile forKey:tile.URL];
  WriteSavedMostVisited(tiles);
}

// Updates the Shortcut's widget with the user's current most visited sites
void UpdateShortcutsWidget() {
#if BUILDFLAG(ENABLE_WIDGET_KIT_EXTENSION)
  [WidgetTimelinesUpdater reloadTimelinesOfKind:@"ShortcutsWidget"];
#endif
}

void WriteSavedMostVisited(NSDictionary<NSURL*, NTPTile*>* most_visited_data) {
  NSDate* last_modification_date = NSDate.date;
  NSError* error = nil;
  NSData* data = [NSKeyedArchiver archivedDataWithRootObject:most_visited_data
                                       requiringSecureCoding:NO
                                                       error:&error];
  if (!data || error) {
    DLOG(WARNING) << "Error serializing most visited: "
                  << base::SysNSStringToUTF8([error description]);
    return;
  }

  NSUserDefaults* sharedDefaults = app_group::GetGroupUserDefaults();

  [sharedDefaults setObject:data forKey:app_group::kSuggestedItems];
  [sharedDefaults setObject:last_modification_date
                     forKey:app_group::kSuggestedItemsLastModificationDate];
  UpdateShortcutsWidget();
}

NSDictionary* ReadSavedMostVisited() {
  NSUserDefaults* sharedDefaults = app_group::GetGroupUserDefaults();
  NSError* error = nil;
  NSKeyedUnarchiver* unarchiver = [[NSKeyedUnarchiver alloc]
      initForReadingFromData:[sharedDefaults
                                 objectForKey:app_group::kSuggestedItems]
                       error:&error];
  if (!unarchiver || error) {
    DLOG(WARNING) << "Error creating unarchiver for most visited: "
                  << base::SysNSStringToUTF8([error description]);
    return [[NSMutableDictionary alloc] init];
  }

  unarchiver.requiresSecureCoding = NO;
  return [unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey];
}

void UpdateSingleFavicon(const GURL& site_url,
                         FaviconAttributesProvider* favicon_provider,
                         NSURL* favicons_directory) {
  NSURL* siteNSURL = net::NSURLWithGURL(site_url);

  void (^faviconAttributesBlock)(FaviconAttributes*) =
      ^(FaviconAttributes* attributes) {
        if (attributes.faviconImage) {
          // Update the available icon.
          // If we have a fallback icon, do not remove it. The favicon will have
          // priority, and should anything happen to the image, the fallback
          // icon will be a nicer fallback.
          NSString* faviconFileName =
              GetFaviconFileName(net::GURLWithNSURL(siteNSURL));
          NSURL* fileURL =
              [favicons_directory URLByAppendingPathComponent:faviconFileName];
          NSData* imageData = UIImagePNGRepresentation(attributes.faviconImage);

          base::OnceCallback<void()> writeImage = base::BindOnce(^{
            base::ScopedBlockingCall scoped_blocking_call(
                FROM_HERE, base::BlockingType::WILL_BLOCK);
            [imageData writeToURL:fileURL atomically:YES];
          });

          base::ThreadPool::PostTaskAndReply(
              FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
              std::move(writeImage), base::BindOnce(&UpdateShortcutsWidget));
        } else {
          NSDictionary* tiles = ReadSavedMostVisited();
          NTPTile* tile = [tiles objectForKey:siteNSURL];
          if (!tile) {
            return;
          }
          tile.fallbackTextColor = attributes.textColor;
          tile.fallbackBackgroundColor = attributes.backgroundColor;
          tile.fallbackIsDefaultColor = attributes.defaultBackgroundColor;
          tile.fallbackMonogram = attributes.monogramString;
          WriteSingleUpdatedTileToDisk(tile);
          // Favicon is outdated. Delete it.
          NSString* faviconFileName =
              GetFaviconFileName(net::GURLWithNSURL(siteNSURL));
          NSURL* fileURL =
              [favicons_directory URLByAppendingPathComponent:faviconFileName];
          base::OnceCallback<void()> removeImage = base::BindOnce(^{
            base::ScopedBlockingCall scoped_blocking_call(
                FROM_HERE, base::BlockingType::WILL_BLOCK);
            [[NSFileManager defaultManager] removeItemAtURL:fileURL error:nil];
          });

          base::ThreadPool::PostTask(
              FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
              std::move(removeImage));
        }
      };

  [favicon_provider fetchFaviconAttributesForURL:site_url
                                      completion:faviconAttributesBlock];
}
}  // namespace content_suggestions_tile_saver