Use Manifest.icons instead of favicon in ShortcutHelper when possible.

The algorithm is first trying to find an image to fit exactly the
required size in the device scale factor and default scale factor.
If it can't it will try to find the closest but preferrable largest
image. Note that the algorithm completely ignore an entry with no 'sizes'.

Fetching the image happens as soon as the Manifest is loaded and can be
done before or after the call to ShortcutHelper::AddShortcut().

BUG=366145
TEST=ShortcutHelperTest

Review URL: https://codereview.chromium.org/601433002

Cr-Commit-Position: refs/heads/master@{#296522}
diff --git a/chrome/browser/android/shortcut_helper.cc b/chrome/browser/android/shortcut_helper.cc
index a485d40..339e26f 100644
--- a/chrome/browser/android/shortcut_helper.cc
+++ b/chrome/browser/android/shortcut_helper.cc
@@ -5,12 +5,14 @@
 #include "chrome/browser/android/shortcut_helper.h"
 
 #include <jni.h>
+#include <limits>
 
 #include "base/android/jni_android.h"
 #include "base/android/jni_string.h"
 #include "base/basictypes.h"
 #include "base/location.h"
 #include "base/strings/string16.h"
+#include "base/strings/utf_string_conversions.h"
 #include "base/task/cancelable_task_tracker.h"
 #include "base/threading/worker_pool.h"
 #include "chrome/browser/android/tab_android.h"
@@ -25,12 +27,20 @@
 #include "content/public/common/frame_navigate_params.h"
 #include "content/public/common/manifest.h"
 #include "jni/ShortcutHelper_jni.h"
+#include "net/base/mime_util.h"
 #include "ui/gfx/android/java_bitmap.h"
 #include "ui/gfx/codec/png_codec.h"
 #include "ui/gfx/color_analysis.h"
 #include "ui/gfx/favicon_size.h"
+#include "ui/gfx/screen.h"
 #include "url/gurl.h"
 
+using content::Manifest;
+
+// Android's preferred icon size in DP is 48, as defined in
+// http://developer.android.com/design/style/iconography.html
+const int ShortcutHelper::kPreferredIconSizeInDp = 48;
+
 jlong Initialize(JNIEnv* env, jobject obj, jlong tab_android_ptr) {
   TabAndroid* tab = reinterpret_cast<TabAndroid*>(tab_android_ptr);
 
@@ -49,6 +59,11 @@
       url_(web_contents->GetURL()),
       display_(content::Manifest::DISPLAY_MODE_BROWSER),
       orientation_(blink::WebScreenOrientationLockDefault),
+      add_shortcut_requested_(false),
+      manifest_icon_status_(MANIFEST_ICON_STATUS_NONE),
+      preferred_icon_size_in_px_(kPreferredIconSizeInDp *
+          gfx::Screen::GetScreenFor(web_contents->GetNativeView())->
+              GetPrimaryDisplay().device_scale_factor()),
       weak_ptr_factory_(this) {
 }
 
@@ -97,6 +112,124 @@
                                          weak_ptr_factory_.GetWeakPtr()));
 }
 
+bool ShortcutHelper::IconSizesContainsPreferredSize(
+    const std::vector<gfx::Size>& sizes) const {
+  for (size_t i = 0; i < sizes.size(); ++i) {
+    if (sizes[i].height() != sizes[i].width())
+      continue;
+    if (sizes[i].width() == preferred_icon_size_in_px_)
+      return true;
+  }
+
+  return false;
+}
+
+bool ShortcutHelper::IconSizesContainsAny(
+    const std::vector<gfx::Size>& sizes) const {
+  for (size_t i = 0; i < sizes.size(); ++i) {
+    if (sizes[i].IsEmpty())
+      return true;
+  }
+
+  return false;
+}
+
+GURL ShortcutHelper::FindBestMatchingIcon(
+    const std::vector<Manifest::Icon>& icons, float density) const {
+  GURL url;
+  int best_delta = std::numeric_limits<int>::min();
+
+  for (size_t i = 0; i < icons.size(); ++i) {
+    if (icons[i].density != density)
+      continue;
+
+    const std::vector<gfx::Size>& sizes = icons[i].sizes;
+    for (size_t j = 0; j < sizes.size(); ++j) {
+      if (sizes[j].height() != sizes[j].width())
+        continue;
+      int delta = sizes[j].width() - preferred_icon_size_in_px_;
+      if (delta == 0)
+        return icons[i].src;
+      if (best_delta > 0 && delta < 0)
+        continue;
+      if ((best_delta > 0 && delta < best_delta) ||
+          (best_delta < 0 && delta > best_delta)) {
+        url = icons[i].src;
+        best_delta = delta;
+      }
+    }
+  }
+
+  return url;
+}
+
+// static
+std::vector<Manifest::Icon> ShortcutHelper::FilterIconsByType(
+    const std::vector<Manifest::Icon>& icons) {
+  std::vector<Manifest::Icon> result;
+
+  for (size_t i = 0; i < icons.size(); ++i) {
+    if (icons[i].type.is_null() ||
+        net::IsSupportedImageMimeType(
+            base::UTF16ToUTF8(icons[i].type.string()))) {
+      result.push_back(icons[i]);
+    }
+  }
+
+  return result;
+}
+
+GURL ShortcutHelper::FindBestMatchingIcon(
+    const std::vector<Manifest::Icon>& unfiltered_icons) const {
+  const float device_scale_factor =
+      gfx::Screen::GetScreenFor(web_contents()->GetNativeView())->
+          GetPrimaryDisplay().device_scale_factor();
+
+  GURL url;
+  std::vector<Manifest::Icon> icons = FilterIconsByType(unfiltered_icons);
+
+  // The first pass is to find the ideal icon. That icon is of the right size
+  // with the default density or the device's density.
+  for (size_t i = 0; i < icons.size(); ++i) {
+    if (icons[i].density == device_scale_factor &&
+        IconSizesContainsPreferredSize(icons[i].sizes)) {
+      return icons[i].src;
+    }
+
+    // If there is an icon with the right size but not the right density, keep
+    // it on the side and only use it if nothing better is found.
+    if (icons[i].density == Manifest::Icon::kDefaultDensity &&
+        IconSizesContainsPreferredSize(icons[i].sizes)) {
+      url = icons[i].src;
+    }
+  }
+
+  // The second pass is to find an icon with 'any'. The current device scale
+  // factor is preferred. Otherwise, the default scale factor is used.
+  for (size_t i = 0; i < icons.size(); ++i) {
+    if (icons[i].density == device_scale_factor &&
+        IconSizesContainsAny(icons[i].sizes)) {
+      return icons[i].src;
+    }
+
+    // If there is an icon with 'any' but not the right density, keep it on the
+    // side and only use it if nothing better is found.
+    if (icons[i].density == Manifest::Icon::kDefaultDensity &&
+        IconSizesContainsAny(icons[i].sizes)) {
+      url = icons[i].src;
+    }
+  }
+
+  // The last pass will try to find the best suitable icon for the device's
+  // scale factor. If none, another pass will be run using kDefaultDensity.
+  if (!url.is_valid())
+    url = FindBestMatchingIcon(icons, device_scale_factor);
+  if (!url.is_valid())
+    url = FindBestMatchingIcon(icons, Manifest::Icon::kDefaultDensity);
+
+  return url;
+}
+
 void ShortcutHelper::OnDidGetManifest(const content::Manifest& manifest) {
   // Set the title based on the manifest value, if any.
   if (!manifest.short_name.is_null())
@@ -128,6 +261,16 @@
       orientation_ = manifest.orientation;
   }
 
+  GURL icon_src = FindBestMatchingIcon(manifest.icons);
+  if (icon_src.is_valid()) {
+    web_contents()->DownloadImage(icon_src,
+                                  false,
+                                  preferred_icon_size_in_px_,
+                                  base::Bind(&ShortcutHelper::OnDidDownloadIcon,
+                                             weak_ptr_factory_.GetWeakPtr()));
+    manifest_icon_status_ = MANIFEST_ICON_STATUS_FETCHING;
+  }
+
   // The ShortcutHelper is now able to notify its Java counterpart that it is
   // initialized. OnInitialized method is not conceptually part of getting the
   // manifest data but it happens that the initialization is finalized when
@@ -140,6 +283,40 @@
   Java_ShortcutHelper_onInitialized(env, j_obj.obj(), j_title.obj());
 }
 
+void ShortcutHelper::OnDidDownloadIcon(int id,
+                                       int http_status_code,
+                                       const GURL& url,
+                                       const std::vector<SkBitmap>& bitmaps,
+                                       const std::vector<gfx::Size>& sizes) {
+  // If getting the candidate manifest icon failed, the ShortcutHelper should
+  // fallback to the favicon.
+  // If the user already requested to add the shortcut, it will do so but use
+  // the favicon instead.
+  // Otherwise, it sets the state as if there was no manifest icon pending.
+  if (bitmaps.empty()) {
+    if (add_shortcut_requested_)
+      AddShortcutUsingFavicon();
+    else
+      manifest_icon_status_ = MANIFEST_ICON_STATUS_NONE;
+    return;
+  }
+
+  // There might be multiple bitmaps returned. The one to pick is bigger or
+  // equal to the preferred size. |bitmaps| is ordered from bigger to smaller.
+  int preferred_bitmap_index = 0;
+  for (size_t i = 0; i < bitmaps.size(); ++i) {
+    if (bitmaps[i].height() < preferred_icon_size_in_px_)
+      break;
+    preferred_bitmap_index = i;
+  }
+
+  manifest_icon_ = bitmaps[preferred_bitmap_index];
+  manifest_icon_status_ = MANIFEST_ICON_STATUS_DONE;
+
+  if (add_shortcut_requested_)
+    AddShortcutUsingManifestIcon();
+}
+
 void ShortcutHelper::TearDown(JNIEnv*, jobject) {
   Destroy();
 }
@@ -153,10 +330,43 @@
     jobject obj,
     jstring jtitle,
     jint launcher_large_icon_size) {
+  add_shortcut_requested_ = true;
+
   base::string16 title = base::android::ConvertJavaStringToUTF16(env, jtitle);
   if (!title.empty())
     title_ = title;
 
+  switch (manifest_icon_status_) {
+    case MANIFEST_ICON_STATUS_NONE:
+      AddShortcutUsingFavicon();
+      break;
+    case MANIFEST_ICON_STATUS_FETCHING:
+      // ::OnDidDownloadIcon() will call AddShortcutUsingManifestIcon().
+      break;
+    case MANIFEST_ICON_STATUS_DONE:
+      AddShortcutUsingManifestIcon();
+      break;
+  }
+}
+
+void ShortcutHelper::AddShortcutUsingManifestIcon() {
+  // Stop observing so we don't get destroyed while doing the last steps.
+  Observe(NULL);
+
+  base::WorkerPool::PostTask(
+      FROM_HERE,
+      base::Bind(&ShortcutHelper::AddShortcutInBackgroundWithSkBitmap,
+                 url_,
+                 title_,
+                 display_,
+                 manifest_icon_,
+                 orientation_),
+      true);
+
+  Destroy();
+}
+
+void ShortcutHelper::AddShortcutUsingFavicon() {
   Profile* profile =
       Profile::FromBrowserContext(web_contents()->GetBrowserContext());
 
@@ -172,28 +382,26 @@
 
   // Using favicon if its size is not smaller than platform required size,
   // otherwise using the largest icon among all avaliable icons.
-  int threshold_to_get_any_largest_icon = launcher_large_icon_size_ - 1;
+  int threshold_to_get_any_largest_icon = preferred_icon_size_in_px_ - 1;
   favicon_service->GetLargestRawFaviconForPageURL(url_, icon_types,
       threshold_to_get_any_largest_icon,
-      base::Bind(&ShortcutHelper::FinishAddingShortcut,
+      base::Bind(&ShortcutHelper::OnDidGetFavicon,
                  base::Unretained(this)),
       &cancelable_task_tracker_);
 }
 
-void ShortcutHelper::FinishAddingShortcut(
+void ShortcutHelper::OnDidGetFavicon(
     const favicon_base::FaviconRawBitmapResult& bitmap_result) {
-  icon_ = bitmap_result;
-
   // Stop observing so we don't get destroyed while doing the last steps.
   Observe(NULL);
 
   base::WorkerPool::PostTask(
       FROM_HERE,
-      base::Bind(&ShortcutHelper::AddShortcutInBackground,
+      base::Bind(&ShortcutHelper::AddShortcutInBackgroundWithRawBitmap,
                  url_,
                  title_,
                  display_,
-                 icon_,
+                 bitmap_result,
                  orientation_),
       true);
 
@@ -220,7 +428,7 @@
   return RegisterNativesImpl(env);
 }
 
-void ShortcutHelper::AddShortcutInBackground(
+void ShortcutHelper::AddShortcutInBackgroundWithRawBitmap(
     const GURL& url,
     const base::string16& title,
     content::Manifest::DisplayMode display,
@@ -228,16 +436,26 @@
     blink::WebScreenOrientationLockType orientation) {
   DCHECK(base::WorkerPool::RunsTasksOnCurrentThread());
 
-  // Grab the average color from the bitmap.
-  SkColor color = SK_ColorWHITE;
-  SkBitmap favicon_bitmap;
+  SkBitmap icon_bitmap;
   if (bitmap_result.is_valid()) {
-    if (gfx::PNGCodec::Decode(bitmap_result.bitmap_data->front(),
-                              bitmap_result.bitmap_data->size(),
-                              &favicon_bitmap))
-      color = color_utils::CalculateKMeanColorOfBitmap(favicon_bitmap);
+    gfx::PNGCodec::Decode(bitmap_result.bitmap_data->front(),
+                          bitmap_result.bitmap_data->size(),
+                          &icon_bitmap);
   }
 
+  AddShortcutInBackgroundWithSkBitmap(
+      url, title, display, icon_bitmap, orientation);
+}
+
+void ShortcutHelper::AddShortcutInBackgroundWithSkBitmap(
+    const GURL& url,
+    const base::string16& title,
+    content::Manifest::DisplayMode display,
+    const SkBitmap& icon_bitmap,
+    blink::WebScreenOrientationLockType orientation) {
+  DCHECK(base::WorkerPool::RunsTasksOnCurrentThread());
+
+  SkColor color = color_utils::CalculateKMeanColorOfBitmap(icon_bitmap);
   int r_value = SkColorGetR(color);
   int g_value = SkColorGetG(color);
   int b_value = SkColorGetB(color);
@@ -249,8 +467,8 @@
   ScopedJavaLocalRef<jstring> java_title =
       base::android::ConvertUTF16ToJavaString(env, title);
   ScopedJavaLocalRef<jobject> java_bitmap;
-  if (favicon_bitmap.getSize())
-    java_bitmap = gfx::ConvertToJavaBitmap(&favicon_bitmap);
+  if (icon_bitmap.getSize())
+    java_bitmap = gfx::ConvertToJavaBitmap(&icon_bitmap);
 
   Java_ShortcutHelper_addShortcut(
       env,
diff --git a/chrome/browser/android/shortcut_helper.h b/chrome/browser/android/shortcut_helper.h
index 4800960..fda61b8 100644
--- a/chrome/browser/android/shortcut_helper.h
+++ b/chrome/browser/android/shortcut_helper.h
@@ -58,41 +58,105 @@
                    jstring title,
                    jint launcher_large_icon_size);
 
-  void FinishAddingShortcut(
+  // Callback run when the requested Manifest icon is ready to be used.
+  void OnDidDownloadIcon(int id,
+                         int http_status_code,
+                         const GURL& url,
+                         const std::vector<SkBitmap>& bitmaps,
+                         const std::vector<gfx::Size>& sizes);
+
+  // Called after AddShortcut() and OnDidDownloadIcon() are run if
+  // OnDidDownloadIcon has a valid icon.
+  void AddShortcutUsingManifestIcon();
+
+  // Use FaviconService to get the best available favicon and create the
+  // shortcut using it. This is used when no Manifest icons are available or
+  // appropriate.
+  void AddShortcutUsingFavicon();
+
+  // Callback run when a favicon is received from GetFavicon() call.
+  void OnDidGetFavicon(
       const favicon_base::FaviconRawBitmapResult& bitmap_result);
 
   // WebContentsObserver
   virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE;
   virtual void WebContentsDestroyed() OVERRIDE;
 
-  // Adds a shortcut to the launcher.  Must be called from a WorkerPool task.
-  static void AddShortcutInBackground(
+  // Adds a shortcut to the launcher using a FaviconRawBitmapResult.
+  // Must be called from a WorkerPool task.
+  static void AddShortcutInBackgroundWithRawBitmap(
       const GURL& url,
       const base::string16& title,
       content::Manifest::DisplayMode display,
       const favicon_base::FaviconRawBitmapResult& bitmap_result,
       blink::WebScreenOrientationLockType orientation);
 
+  // Adds a shortcut to the launcher using a SkBitmap.
+  // Must be called from a WorkerPool task.
+  static void AddShortcutInBackgroundWithSkBitmap(
+      const GURL& url,
+      const base::string16& title,
+      content::Manifest::DisplayMode display,
+      const SkBitmap& icon_bitmap,
+      blink::WebScreenOrientationLockType orientation);
+
   // Registers JNI hooks.
   static bool RegisterShortcutHelper(JNIEnv* env);
 
  private:
+  enum ManifestIconStatus {
+    MANIFEST_ICON_STATUS_NONE,
+    MANIFEST_ICON_STATUS_FETCHING,
+    MANIFEST_ICON_STATUS_DONE
+  };
+
   virtual ~ShortcutHelper();
 
   void Destroy();
 
+  // Runs the algorithm to find the best matching icon in the icons listed in
+  // the Manifest.
+  // Returns the icon url if a suitable icon is found. An empty URL otherwise.
+  GURL FindBestMatchingIcon(
+      const std::vector<content::Manifest::Icon>& icons) const;
+
+  // Runs an algorithm only based on icon declared sizes. It will try to find
+  // size that is the closest to preferred_icon_size_in_px_ but bigger than
+  // preferred_icon_size_in_px_ if possible.
+  // Returns the icon url if a suitable icon is found. An empty URL otherwise.
+  GURL FindBestMatchingIcon(const std::vector<content::Manifest::Icon>& icons,
+                            float density) const;
+
+  // Returns an array containing the items in |icons| without the unsupported
+  // image MIME types.
+  static std::vector<content::Manifest::Icon> FilterIconsByType(
+      const std::vector<content::Manifest::Icon>& icons);
+
+  // Returns whether the preferred_icon_size_in_px_ is in the given |sizes|.
+  bool IconSizesContainsPreferredSize(
+      const std::vector<gfx::Size>& sizes) const;
+
+  // Returns whether the 'any' (ie. gfx::Size(0,0)) is in the given |sizes|.
+  bool IconSizesContainsAny(const std::vector<gfx::Size>& sizes) const;
+
   JavaObjectWeakGlobalRef java_ref_;
 
   GURL url_;
   base::string16 title_;
-  int launcher_large_icon_size_;
   content::Manifest::DisplayMode display_;
-  favicon_base::FaviconRawBitmapResult icon_;
+  SkBitmap manifest_icon_;
   base::CancelableTaskTracker cancelable_task_tracker_;
   blink::WebScreenOrientationLockType orientation_;
 
+  bool add_shortcut_requested_;
+
+  ManifestIconStatus manifest_icon_status_;
+  const int preferred_icon_size_in_px_;
+  static const int kPreferredIconSizeInDp;
+
   base::WeakPtrFactory<ShortcutHelper> weak_ptr_factory_;
 
+  friend class ShortcutHelperTest;
   DISALLOW_COPY_AND_ASSIGN(ShortcutHelper);
 };
 
diff --git a/chrome/browser/android/shortcut_helper_unittest.cc b/chrome/browser/android/shortcut_helper_unittest.cc
new file mode 100644
index 0000000..98b35ce
--- /dev/null
+++ b/chrome/browser/android/shortcut_helper_unittest.cc
@@ -0,0 +1,495 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/android/shortcut_helper.h"
+
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "content/public/browser/web_contents.h"
+#include "ui/gfx/screen.h"
+#include "ui/gfx/screen_type_delegate.h"
+
+// A dummy implementation of gfx::Screen, since ShortcutHelper needs access to
+// a gfx::Display's device scale factor.
+// This is inspired by web_contents_video_capture_device_unittest.cc
+// A bug has been opened to merge all those mocks: http://crbug.com/417227
+class FakeScreen : public gfx::Screen {
+ public:
+  FakeScreen() : display_(0x1337, gfx::Rect(0, 0, 2560, 1440)) {
+  }
+  virtual ~FakeScreen() {}
+
+  void SetDisplayDeviceScaleFactor(float device_scale_factor) {
+    display_.set_device_scale_factor(device_scale_factor);
+  }
+
+  // gfx::Screen implementation (only what's needed for testing).
+  virtual bool IsDIPEnabled() OVERRIDE { return true; }
+  virtual gfx::Point GetCursorScreenPoint() OVERRIDE { return gfx::Point(); }
+  virtual gfx::NativeWindow GetWindowUnderCursor() OVERRIDE { return NULL; }
+  virtual gfx::NativeWindow GetWindowAtScreenPoint(
+      const gfx::Point& point) OVERRIDE { return NULL; }
+  virtual int GetNumDisplays() const OVERRIDE { return 1; }
+  virtual std::vector<gfx::Display> GetAllDisplays() const OVERRIDE {
+    return std::vector<gfx::Display>(1, display_);
+  }
+  virtual gfx::Display GetDisplayNearestWindow(
+      gfx::NativeView view) const OVERRIDE {
+    return display_;
+  }
+  virtual gfx::Display GetDisplayNearestPoint(
+      const gfx::Point& point) const OVERRIDE {
+    return display_;
+  }
+  virtual gfx::Display GetDisplayMatching(
+      const gfx::Rect& match_rect) const OVERRIDE {
+    return display_;
+  }
+  virtual gfx::Display GetPrimaryDisplay() const OVERRIDE {
+    return display_;
+  }
+  virtual void AddObserver(gfx::DisplayObserver* observer) OVERRIDE {}
+  virtual void RemoveObserver(gfx::DisplayObserver* observer) OVERRIDE {}
+
+ private:
+  gfx::Display display_;
+
+  DISALLOW_COPY_AND_ASSIGN(FakeScreen);
+};
+
+class ShortcutHelperTest : public ChromeRenderViewHostTestHarness  {
+ protected:
+  ShortcutHelperTest() : shortcut_helper_(NULL) {}
+  virtual ~ShortcutHelperTest() {}
+
+  static jobject CreateShortcutHelperJava(JNIEnv* env) {
+    jclass clazz = env->FindClass("org/chromium/chrome/browser/ShortcutHelper");
+    jmethodID constructor =
+        env->GetMethodID(clazz, "<init>",
+                         "(Landroid/content/Context;"
+                             "Lorg/chromium/chrome/browser/Tab;)V");
+    return env->NewObject(clazz, constructor, jobject(), jobject());
+  }
+
+  void ResetShorcutHelper() {
+    if (shortcut_helper_)
+      delete shortcut_helper_;
+
+    JNIEnv* env = base::android::AttachCurrentThread();
+    shortcut_helper_ =
+        new ShortcutHelper(env, CreateShortcutHelperJava(env), web_contents());
+  }
+
+  virtual void SetUp() OVERRIDE {
+    gfx::Screen::SetScreenInstance(gfx::SCREEN_TYPE_NATIVE, &fake_screen_);
+    ASSERT_EQ(&fake_screen_, gfx::Screen::GetNativeScreen());
+
+    ChromeRenderViewHostTestHarness::SetUp();
+
+    ResetShorcutHelper();
+  }
+
+  virtual void TearDown() OVERRIDE {
+    delete shortcut_helper_;
+    shortcut_helper_ = NULL;
+
+    ChromeRenderViewHostTestHarness::TearDown();
+
+    gfx::Screen::SetScreenInstance(gfx::SCREEN_TYPE_NATIVE, NULL);
+  }
+
+  GURL FindBestMatchingIcon(const std::vector<content::Manifest::Icon>& icons) {
+    return shortcut_helper_->FindBestMatchingIcon(icons);
+  }
+
+  void SetDisplayDeviceScaleFactor(float device_scale_factor) {
+    fake_screen_.SetDisplayDeviceScaleFactor(device_scale_factor);
+
+    ResetShorcutHelper();
+  }
+
+  static int GetPreferredIconSizeInDp() {
+    return ShortcutHelper::kPreferredIconSizeInDp;
+  }
+
+  static content::Manifest::Icon CreateIcon(
+      const std::string& url,
+      const std::string& type,
+      double density,
+      const std::vector<gfx::Size> sizes) {
+    content::Manifest::Icon icon;
+    icon.src = GURL(url);
+    if (!type.empty())
+      icon.type = base::NullableString16(base::UTF8ToUTF16(type), false);
+    icon.density = density;
+    icon.sizes = sizes;
+
+    return icon;
+  }
+
+ private:
+  ShortcutHelper* shortcut_helper_;
+  FakeScreen fake_screen_;
+
+  DISALLOW_COPY_AND_ASSIGN(ShortcutHelperTest);
+};
+
+TEST_F(ShortcutHelperTest, NoIcons) {
+  // No icons should return the empty URL.
+  std::vector<content::Manifest::Icon> icons;
+  GURL url = FindBestMatchingIcon(icons);
+  EXPECT_TRUE(url.is_empty());
+}
+
+TEST_F(ShortcutHelperTest, NoSizes) {
+  // Icon with no sizes are ignored.
+  std::vector<content::Manifest::Icon> icons;
+  icons.push_back(
+      CreateIcon("http://foo.com/icon.png", "", 1.0, std::vector<gfx::Size>()));
+
+  GURL url = FindBestMatchingIcon(icons);
+  EXPECT_TRUE(url.is_empty());
+}
+
+TEST_F(ShortcutHelperTest, MIMETypeFiltering) {
+  // Icons with type specified to a MIME type that isn't a valid image MIME type
+  // are ignored.
+  std::vector<gfx::Size> sizes;
+  sizes.push_back(gfx::Size(10, 10));
+
+  std::vector<content::Manifest::Icon> icons;
+  icons.push_back(
+      CreateIcon("http://foo.com/icon.png", "image/foo_bar", 1.0, sizes));
+  icons.push_back(CreateIcon("http://foo.com/icon.png", "image/", 1.0, sizes));
+  icons.push_back(CreateIcon("http://foo.com/icon.png", "image/", 1.0, sizes));
+  icons.push_back(
+      CreateIcon("http://foo.com/icon.png", "video/mp4", 1.0, sizes));
+
+  GURL url = FindBestMatchingIcon(icons);
+  EXPECT_TRUE(url.is_empty());
+
+  icons.clear();
+  icons.push_back(
+      CreateIcon("http://foo.com/icon.png", "image/png", 1.0, sizes));
+  url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon.png", url.spec());
+
+  icons.clear();
+  icons.push_back(
+      CreateIcon("http://foo.com/icon.png", "image/gif", 1.0, sizes));
+  url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon.png", url.spec());
+
+  icons.clear();
+  icons.push_back(
+      CreateIcon("http://foo.com/icon.png", "image/jpeg", 1.0, sizes));
+  url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon.png", url.spec());
+}
+
+TEST_F(ShortcutHelperTest, PreferredSizeOfCurrentDensityIsUsedFirst) {
+  // This test has three icons each are marked with sizes set to the preferred
+  // icon size for the associated density.
+  std::vector<gfx::Size> sizes_1;
+  sizes_1.push_back(gfx::Size(GetPreferredIconSizeInDp(),
+                              GetPreferredIconSizeInDp()));
+
+  std::vector<gfx::Size> sizes_2;
+  sizes_2.push_back(gfx::Size(GetPreferredIconSizeInDp() * 2,
+                              GetPreferredIconSizeInDp() * 2));
+
+  std::vector<gfx::Size> sizes_3;
+  sizes_3.push_back(gfx::Size(GetPreferredIconSizeInDp() * 3,
+                              GetPreferredIconSizeInDp() * 3));
+
+  std::vector<content::Manifest::Icon> icons;
+  icons.push_back(CreateIcon("http://foo.com/icon_x1.png", "", 1.0, sizes_1));
+  icons.push_back(CreateIcon("http://foo.com/icon_x2.png", "", 2.0, sizes_2));
+  icons.push_back(CreateIcon("http://foo.com/icon_x3.png", "", 3.0, sizes_3));
+
+  SetDisplayDeviceScaleFactor(1.0f);
+  GURL url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon_x1.png", url.spec());
+
+  SetDisplayDeviceScaleFactor(2.0f);
+  url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon_x2.png", url.spec());
+
+  SetDisplayDeviceScaleFactor(3.0f);
+  url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon_x3.png", url.spec());
+}
+
+TEST_F(ShortcutHelperTest, PreferredSizeOfDefaultDensityIsUsedSecond) {
+  // This test has three icons. The first one is of density zero and is marked
+  // with three sizes which are the preferred icon size for density 1, 2 and 3.
+  // The icon for density 2 and 3 have a size set to 2x2 and 3x3.
+  // Regardless of the device scale factor, the icon of density 1 is going to be
+  // used because it matches the preferred size.
+  std::vector<gfx::Size> sizes_1;
+  sizes_1.push_back(gfx::Size(GetPreferredIconSizeInDp(),
+                              GetPreferredIconSizeInDp()));
+  sizes_1.push_back(gfx::Size(GetPreferredIconSizeInDp() * 2,
+                              GetPreferredIconSizeInDp() * 2));
+  sizes_1.push_back(gfx::Size(GetPreferredIconSizeInDp() * 3,
+                              GetPreferredIconSizeInDp() * 3));
+
+  std::vector<gfx::Size> sizes_2;
+  sizes_2.push_back(gfx::Size(2, 2));
+
+  std::vector<gfx::Size> sizes_3;
+  sizes_3.push_back(gfx::Size(3, 3));
+
+  std::vector<content::Manifest::Icon> icons;
+  icons.push_back(CreateIcon("http://foo.com/icon_x1.png", "", 1.0, sizes_1));
+  icons.push_back(CreateIcon("http://foo.com/icon_x2.png", "", 2.0, sizes_2));
+  icons.push_back(CreateIcon("http://foo.com/icon_x3.png", "", 3.0, sizes_3));
+
+  SetDisplayDeviceScaleFactor(1.0f);
+  GURL url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon_x1.png", url.spec());
+
+  SetDisplayDeviceScaleFactor(2.0f);
+  url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon_x1.png", url.spec());
+
+  SetDisplayDeviceScaleFactor(3.0f);
+  url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon_x1.png", url.spec());
+}
+
+TEST_F(ShortcutHelperTest, DeviceDensityFirst) {
+  // If there is no perfect icon but an icon of the current device density is
+  // present, it will be picked.
+  // This test has three icons each are marked with sizes set to the preferred
+  // icon size for the associated density.
+  std::vector<gfx::Size> sizes;
+  sizes.push_back(gfx::Size(2, 2));
+
+  std::vector<content::Manifest::Icon> icons;
+  icons.push_back(CreateIcon("http://foo.com/icon_x1.png", "", 1.0, sizes));
+  icons.push_back(CreateIcon("http://foo.com/icon_x2.png", "", 2.0, sizes));
+  icons.push_back(CreateIcon("http://foo.com/icon_x3.png", "", 3.0, sizes));
+
+  SetDisplayDeviceScaleFactor(1.0f);
+  GURL url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon_x1.png", url.spec());
+
+  SetDisplayDeviceScaleFactor(2.0f);
+  url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon_x2.png", url.spec());
+
+  SetDisplayDeviceScaleFactor(3.0f);
+  url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon_x3.png", url.spec());
+}
+
+TEST_F(ShortcutHelperTest, DeviceDensityFallback) {
+  // If there is no perfect icon but and no icon of the current display density,
+  // an icon of density 1.0 will be used.
+  std::vector<gfx::Size> sizes;
+  sizes.push_back(gfx::Size(2, 2));
+
+  std::vector<content::Manifest::Icon> icons;
+  icons.push_back(CreateIcon("http://foo.com/icon_x1.png", "", 1.0, sizes));
+  icons.push_back(CreateIcon("http://foo.com/icon_x2.png", "", 2.0, sizes));
+
+  SetDisplayDeviceScaleFactor(3.0f);
+  GURL url = FindBestMatchingIcon(icons);
+  EXPECT_EQ("http://foo.com/icon_x1.png", url.spec());
+}
+
+TEST_F(ShortcutHelperTest, DoNotUseOtherDensities) {
+  // If there are only icons of densities that are not the current display
+  // density or the default density, they are ignored.
+  std::vector<gfx::Size> sizes;
+  sizes.push_back(gfx::Size(2, 2));
+
+  std::vector<content::Manifest::Icon> icons;
+  icons.push_back(CreateIcon("http://foo.com/icon_x2.png", "", 2.0, sizes));
+
+  SetDisplayDeviceScaleFactor(3.0f);
+  GURL url = FindBestMatchingIcon(icons);
+  EXPECT_TRUE(url.is_empty());
+}
+
+TEST_F(ShortcutHelperTest, NotSquareIconsAreIgnored) {
+  std::vector<gfx::Size> sizes;
+  sizes.push_back(gfx::Size(20, 2));
+
+  std::vector<content::Manifest::Icon> icons;
+  icons.push_back(CreateIcon("http://foo.com/icon.png", "", 1.0, sizes));
+
+  GURL url = FindBestMatchingIcon(icons);
+  EXPECT_TRUE(url.is_empty());
+}
+
+TEST_F(ShortcutHelperTest, ClosestIconToPreferred) {
+  // This test verifies ShortcutHelper::FindBestMatchingIcon by passing
+  // different icon sizes and checking which one is picked.
+  // The Device Scale Factor is 1.0 and the preferred icon size is returned by
+  // GetPreferredIconSizeInDp().
+  int very_small = GetPreferredIconSizeInDp() / 4;
+  int small = GetPreferredIconSizeInDp() / 2;
+  int bit_small = GetPreferredIconSizeInDp() - 1;
+  int bit_big = GetPreferredIconSizeInDp() + 1;
+  int big = GetPreferredIconSizeInDp() * 2;
+  int very_big = GetPreferredIconSizeInDp() * 4;
+
+  // (very_small, bit_small) => bit_small
+  {
+    std::vector<gfx::Size> sizes_1;
+    sizes_1.push_back(gfx::Size(very_small, very_small));
+
+    std::vector<gfx::Size> sizes_2;
+    sizes_2.push_back(gfx::Size(bit_small, bit_small));
+
+    std::vector<content::Manifest::Icon> icons;
+    icons.push_back(CreateIcon("http://foo.com/icon_no.png", "", 1.0, sizes_1));
+    icons.push_back(CreateIcon("http://foo.com/icon.png", "", 1.0, sizes_2));
+
+    GURL url = FindBestMatchingIcon(icons);
+    EXPECT_EQ("http://foo.com/icon.png", url.spec());
+  }
+
+  // (very_small, bit_small, smaller) => bit_small
+  {
+    std::vector<gfx::Size> sizes_1;
+    sizes_1.push_back(gfx::Size(very_small, very_small));
+
+    std::vector<gfx::Size> sizes_2;
+    sizes_2.push_back(gfx::Size(bit_small, bit_small));
+
+    std::vector<gfx::Size> sizes_3;
+    sizes_3.push_back(gfx::Size(small, small));
+
+    std::vector<content::Manifest::Icon> icons;
+    icons.push_back(CreateIcon("http://foo.com/icon_no.png", "", 1.0, sizes_1));
+    icons.push_back(CreateIcon("http://foo.com/icon.png", "", 1.0, sizes_2));
+    icons.push_back(CreateIcon("http://foo.com/icon_no.png", "", 1.0, sizes_3));
+
+    GURL url = FindBestMatchingIcon(icons);
+    EXPECT_EQ("http://foo.com/icon.png", url.spec());
+  }
+
+  // (very_big, big) => big
+  {
+    std::vector<gfx::Size> sizes_1;
+    sizes_1.push_back(gfx::Size(very_big, very_big));
+
+    std::vector<gfx::Size> sizes_2;
+    sizes_2.push_back(gfx::Size(big, big));
+
+    std::vector<content::Manifest::Icon> icons;
+    icons.push_back(CreateIcon("http://foo.com/icon_no.png", "", 1.0, sizes_1));
+    icons.push_back(CreateIcon("http://foo.com/icon.png", "", 1.0, sizes_2));
+
+    GURL url = FindBestMatchingIcon(icons);
+    EXPECT_EQ("http://foo.com/icon.png", url.spec());
+  }
+
+  // (very_big, big, bit_big) => bit_big
+  {
+    std::vector<gfx::Size> sizes_1;
+    sizes_1.push_back(gfx::Size(very_big, very_big));
+
+    std::vector<gfx::Size> sizes_2;
+    sizes_2.push_back(gfx::Size(big, big));
+
+    std::vector<gfx::Size> sizes_3;
+    sizes_3.push_back(gfx::Size(bit_big, bit_big));
+
+    std::vector<content::Manifest::Icon> icons;
+    icons.push_back(CreateIcon("http://foo.com/icon_no.png", "", 1.0, sizes_1));
+    icons.push_back(CreateIcon("http://foo.com/icon_no.png", "", 1.0, sizes_2));
+    icons.push_back(CreateIcon("http://foo.com/icon.png", "", 1.0, sizes_3));
+
+    GURL url = FindBestMatchingIcon(icons);
+    EXPECT_EQ("http://foo.com/icon.png", url.spec());
+  }
+
+  // (bit_small, very_big) => very_big
+  {
+    std::vector<gfx::Size> sizes_1;
+    sizes_1.push_back(gfx::Size(bit_small, bit_small));
+
+    std::vector<gfx::Size> sizes_2;
+    sizes_2.push_back(gfx::Size(very_big, very_big));
+
+    std::vector<content::Manifest::Icon> icons;
+    icons.push_back(CreateIcon("http://foo.com/icon_no.png", "", 1.0, sizes_1));
+    icons.push_back(CreateIcon("http://foo.com/icon.png", "", 1.0, sizes_2));
+
+    GURL url = FindBestMatchingIcon(icons);
+    EXPECT_EQ("http://foo.com/icon.png", url.spec());
+  }
+
+  // (bit_small, bit_big) => bit_big
+  {
+    std::vector<gfx::Size> sizes_1;
+    sizes_1.push_back(gfx::Size(bit_small, bit_small));
+
+    std::vector<gfx::Size> sizes_2;
+    sizes_2.push_back(gfx::Size(bit_big, bit_big));
+
+    std::vector<content::Manifest::Icon> icons;
+    icons.push_back(CreateIcon("http://foo.com/icon_no.png", "", 1.0, sizes_1));
+    icons.push_back(CreateIcon("http://foo.com/icon.png", "", 1.0, sizes_2));
+
+    GURL url = FindBestMatchingIcon(icons);
+    EXPECT_EQ("http://foo.com/icon.png", url.spec());
+  }
+}
+
+TEST_F(ShortcutHelperTest, UseAnyIfNoPreferredSize) {
+  // 'any' (ie. gfx::Size(0,0)) should be used if there is no icon of a
+  // preferred size. An icon with the current device scale factor is preferred
+  // over one with the default density.
+
+  // 'any' with preferred size => preferred size
+  {
+    std::vector<gfx::Size> sizes_1;
+    sizes_1.push_back(gfx::Size(GetPreferredIconSizeInDp(),
+                                GetPreferredIconSizeInDp()));
+    std::vector<gfx::Size> sizes_2;
+    sizes_2.push_back(gfx::Size(0,0));
+
+    std::vector<content::Manifest::Icon> icons;
+    icons.push_back(CreateIcon("http://foo.com/icon.png", "", 1.0, sizes_1));
+    icons.push_back(CreateIcon("http://foo.com/icon_no.png", "", 1.0, sizes_2));
+
+    GURL url = FindBestMatchingIcon(icons);
+    EXPECT_EQ("http://foo.com/icon.png", url.spec());
+  }
+
+  // 'any' with nearly preferred size => any
+  {
+    std::vector<gfx::Size> sizes_1;
+    sizes_1.push_back(gfx::Size(GetPreferredIconSizeInDp() + 1,
+                                GetPreferredIconSizeInDp() + 1));
+    std::vector<gfx::Size> sizes_2;
+    sizes_2.push_back(gfx::Size(0,0));
+
+    std::vector<content::Manifest::Icon> icons;
+    icons.push_back(CreateIcon("http://foo.com/icon_no.png", "", 1.0, sizes_1));
+    icons.push_back(CreateIcon("http://foo.com/icon.png", "", 1.0, sizes_2));
+
+    GURL url = FindBestMatchingIcon(icons);
+    EXPECT_EQ("http://foo.com/icon.png", url.spec());
+  }
+
+  // 'any' on default density and current density => current density.
+  {
+    std::vector<gfx::Size> sizes;
+    sizes.push_back(gfx::Size(0,0));
+
+    std::vector<content::Manifest::Icon> icons;
+    icons.push_back(CreateIcon("http://foo.com/icon_no.png", "", 1.0, sizes));
+    icons.push_back(CreateIcon("http://foo.com/icon.png", "", 3.0, sizes));
+
+    SetDisplayDeviceScaleFactor(3.0f);
+    GURL url = FindBestMatchingIcon(icons);
+    EXPECT_EQ("http://foo.com/icon.png", url.spec());
+  }
+}