chromium/ios/chrome/browser/favicon/model/favicon_loader.mm

// Copyright 2015 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/favicon/model/favicon_loader.h"

#import <UIKit/UIKit.h>

#import "base/apple/foundation_util.h"
#import "base/functional/bind.h"
#import "base/strings/sys_string_conversions.h"
#import "components/favicon/core/fallback_url_util.h"
#import "components/favicon/core/large_icon_service.h"
#import "components/favicon_base/fallback_icon_style.h"
#import "components/favicon_base/favicon_callback.h"
#import "components/favicon_base/favicon_types.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/common/ui/favicon/favicon_attributes.h"
#import "net/traffic_annotation/network_traffic_annotation.h"
#import "skia/ext/skia_utils_ios.h"
#import "url/gurl.h"

namespace {
const CGFloat kFallbackIconDefaultTextColor = 0xAAAAAA;

// NetworkTrafficAnnotationTag for fetching favicon from a Google server.
const net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("favicon_loader_get_large_icon", R"(
        semantics {
        sender: "FaviconLoader"
        description:
            "Sends a request to a Google server to retrieve the favicon bitmap."
        trigger:
            "A request can be sent if Chrome does not have a favicon."
        data: "Page URL and desired icon size."
        destination: GOOGLE_OWNED_SERVICE
        }
        policy {
        cookies_allowed: NO
        setting: "This feature cannot be disabled by settings."
        policy_exception_justification: "Not implemented."
        }
        )");
}  // namespace

FaviconLoader::FaviconLoader(favicon::LargeIconService* large_icon_service)
    : large_icon_service_(large_icon_service),
      favicon_cache_([[NSCache alloc] init]) {}
FaviconLoader::~FaviconLoader() {}

// TODO(pinkerton): How do we update the favicon if it's changed on the web?
// We can possibly just rely on this class being purged or the app being killed
// to reset it, but then how do we ensure the FaviconService is updated?
void FaviconLoader::FaviconForPageUrl(
    const GURL& page_url,
    float size_in_points,
    float min_size_in_points,
    bool fallback_to_google_server,  // retrieve favicon from Google Server if
                                     // GetLargeIconOrFallbackStyle() doesn't
                                     // return valid favicon.
    FaviconAttributesCompletionBlock favicon_block_handler) {
  DCHECK(favicon_block_handler);
  NSString* key =
      [NSString stringWithFormat:@"%d %@", (int)round(size_in_points),
                                 base::SysUTF8ToNSString(page_url.spec())];
  FaviconAttributes* value = [favicon_cache_ objectForKey:key];
  if (value) {
    favicon_block_handler(value);
    return;
  }

  const CGFloat scale = UIScreen.mainScreen.scale;
  GURL block_page_url(page_url);
  auto favicon_block = ^(const favicon_base::LargeIconResult& result) {
    // GetLargeIconOrFallbackStyle() either returns a valid favicon (which can
    // be the default favicon) or fallback attributes.
    if (result.bitmap.is_valid()) {
      scoped_refptr<base::RefCountedMemory> data =
          result.bitmap.bitmap_data.get();
      // The favicon code assumes favicons are PNG-encoded.
      UIImage* favicon = [UIImage
          imageWithData:[NSData dataWithBytes:data->front() length:data->size()]
                  scale:scale];
      FaviconAttributes* attributes =
          [FaviconAttributes attributesWithImage:favicon];
      [favicon_cache_ setObject:attributes forKey:key];

      DCHECK(favicon.size.width <= size_in_points &&
             favicon.size.height <= size_in_points);
      favicon_block_handler(attributes);
      return;
    } else if (fallback_to_google_server) {
      void (^favicon_loaded_from_server_block)(
          favicon_base::GoogleFaviconServerRequestStatus status) =
          ^(const favicon_base::GoogleFaviconServerRequestStatus status) {
            // Update the time when the icon was last requested - postpone thus
            // the automatic eviction of the favicon from the favicon database.
            large_icon_service_->TouchIconFromGoogleServer(block_page_url);

            // Favicon should be loaded to the db that backs LargeIconService
            // now.  Fetch it again. Even if the request was not successful, the
            // fallback style will be used.
            FaviconForPageUrl(
                block_page_url, size_in_points, min_size_in_points,
                /*continueToGoogleServer=*/false, favicon_block_handler);
          };

      large_icon_service_
          ->GetLargeIconOrFallbackStyleFromGoogleServerSkippingLocalCache(
              block_page_url,
              /*should_trim_page_url_path=*/false, kTrafficAnnotation,
              base::BindRepeating(favicon_loaded_from_server_block));
      return;
    }

    // Did not get valid favicon back and are not attempting to retrieve one
    // from a Google Server.
    DCHECK(result.fallback_icon_style);
    FaviconAttributes* attributes = [FaviconAttributes
        attributesWithMonogram:base::SysUTF16ToNSString(
                                   favicon::GetFallbackIconText(block_page_url))
                     textColor:UIColorFromRGB(kFallbackIconDefaultTextColor)
               backgroundColor:UIColor.clearColor
        defaultBackgroundColor:result.fallback_icon_style->
                               is_default_background_color];
    favicon_block_handler(attributes);
  };

  // First, synchronously return a fallback image.
  favicon_block_handler([FaviconAttributes attributesWithDefaultImage]);

  // Now fetch the image synchronously.
  DCHECK(large_icon_service_);
  large_icon_service_->GetLargeIconRawBitmapOrFallbackStyleForPageUrl(
      page_url, scale * min_size_in_points, scale * size_in_points,
      base::BindRepeating(favicon_block), &cancelable_task_tracker_);
}

void FaviconLoader::FaviconForPageUrlOrHost(
    const GURL& page_url,
    float size_in_points,
    FaviconAttributesCompletionBlock favicon_block_handler) {
  DCHECK(favicon_block_handler);
  NSString* key = [NSString
      stringWithFormat:@"%d %@ with fallback", (int)round(size_in_points),
                       base::SysUTF8ToNSString(page_url.spec())];
  FaviconAttributes* value = [favicon_cache_ objectForKey:key];
  if (value) {
    favicon_block_handler(value);
    return;
  }

  const CGFloat scale = UIScreen.mainScreen.scale;
  GURL block_page_url(page_url);
  auto favicon_block = ^(const favicon_base::LargeIconResult& result) {
    // GetLargeIconOrFallbackStyle() either returns a valid favicon (which can
    // be the default favicon) or fallback attributes.
    if (result.bitmap.is_valid()) {
      scoped_refptr<base::RefCountedMemory> data =
          result.bitmap.bitmap_data.get();
      // The favicon code assumes favicons are PNG-encoded.
      UIImage* favicon = [UIImage
          imageWithData:[NSData dataWithBytes:data->front() length:data->size()]
                  scale:scale];
      FaviconAttributes* attributes =
          [FaviconAttributes attributesWithImage:favicon];
      [favicon_cache_ setObject:attributes forKey:key];

      DCHECK(favicon.size.width <= size_in_points &&
             favicon.size.height <= size_in_points);
      favicon_block_handler(attributes);
      return;
    }

    // Did not get valid favicon back and are not attempting to retrieve one
    // from a Google Server.
    DCHECK(result.fallback_icon_style);
    FaviconAttributes* attributes = [FaviconAttributes
        attributesWithMonogram:base::SysUTF16ToNSString(
                                   favicon::GetFallbackIconText(block_page_url))
                     textColor:UIColorFromRGB(kFallbackIconDefaultTextColor)
               backgroundColor:UIColor.clearColor
        defaultBackgroundColor:result.fallback_icon_style->
                               is_default_background_color];
    favicon_block_handler(attributes);
  };

  // First, synchronously return a fallback image.
  favicon_block_handler([FaviconAttributes attributesWithDefaultImage]);

  // Now fetch the image synchronously.
  DCHECK(large_icon_service_);
  large_icon_service_->GetIconRawBitmapOrFallbackStyleForPageUrl(
      page_url, scale * size_in_points, base::BindRepeating(favicon_block),
      &cancelable_task_tracker_);
}

void FaviconLoader::FaviconForIconUrl(
    const GURL& icon_url,
    float size_in_points,
    float min_size_in_points,
    FaviconAttributesCompletionBlock favicon_block_handler) {
  DCHECK(favicon_block_handler);
  NSString* key =
      [NSString stringWithFormat:@"%d %@", (int)round(size_in_points),
                                 base::SysUTF8ToNSString(icon_url.spec())];
  FaviconAttributes* value = [favicon_cache_ objectForKey:key];
  if (value) {
    favicon_block_handler(value);
    return;
  }

  const CGFloat scale = UIScreen.mainScreen.scale;
  const CGFloat favicon_size_in_pixels = scale * size_in_points;
  const CGFloat min_favicon_size_in_pixels = scale * min_size_in_points;
  GURL block_icon_url(icon_url);
  auto favicon_block = ^(const favicon_base::LargeIconResult& result) {
    // GetLargeIconOrFallbackStyle() either returns a valid favicon (which can
    // be the default favicon) or fallback attributes.
    if (result.bitmap.is_valid()) {
      scoped_refptr<base::RefCountedMemory> data =
          result.bitmap.bitmap_data.get();
      // The favicon code assumes favicons are PNG-encoded.
      UIImage* favicon = [UIImage
          imageWithData:[NSData dataWithBytes:data->front() length:data->size()]
                  scale:scale];
      FaviconAttributes* attributes =
          [FaviconAttributes attributesWithImage:favicon];
      [favicon_cache_ setObject:attributes forKey:key];
      favicon_block_handler(attributes);
      return;
    }
    // Did not get valid favicon back and are not attempting to retrieve one
    // from a Google Server
    DCHECK(result.fallback_icon_style);
    FaviconAttributes* attributes = [FaviconAttributes
        attributesWithMonogram:base::SysUTF16ToNSString(
                                   favicon::GetFallbackIconText(block_icon_url))
                     textColor:UIColorFromRGB(kFallbackIconDefaultTextColor)
               backgroundColor:UIColor.clearColor
        defaultBackgroundColor:result.fallback_icon_style->
                               is_default_background_color];
    favicon_block_handler(attributes);
  };

  // First, return a fallback synchronously.
  favicon_block_handler([FaviconAttributes
      attributesWithImage:[UIImage imageNamed:@"default_world_favicon"]]);

  // Now call the service for a better async icon.
  DCHECK(large_icon_service_);
  large_icon_service_->GetLargeIconRawBitmapOrFallbackStyleForIconUrl(
      icon_url, min_favicon_size_in_pixels, favicon_size_in_pixels,
      base::BindRepeating(favicon_block), &cancelable_task_tracker_);
}

void FaviconLoader::CancellAllRequests() {
  cancelable_task_tracker_.TryCancelAll();
}

base::WeakPtr<FaviconLoader> FaviconLoader::AsWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}