Refactor ShortcutHelper and related classes to add icon to dialog

In order to get the icon delivered to the Java AddToHomeScreenDialog,
the pipeline has to be redone so that the icon is fetched and processed
asynchronously.  The dialog waits until the icon is available until
letting the user add the page to their homescreen.

This class breaks apart the ShortcutHelper class into two, making the
ownership semantics less prone to premature deletions, and adds a new
event for alerting the Java-side dialog that the icon is available.

Not done yet:
Add a timeout for waiting for the icon.

BUG=482157,457424,412391

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

Cr-Commit-Position: refs/heads/master@{#328482}
diff --git a/chrome/browser/android/shortcut_helper.cc b/chrome/browser/android/shortcut_helper.cc
index d9007c7..fc46ef7 100644
--- a/chrome/browser/android/shortcut_helper.cc
+++ b/chrome/browser/android/shortcut_helper.cc
@@ -5,7 +5,6 @@
 #include "chrome/browser/android/shortcut_helper.h"
 
 #include <jni.h>
-#include <limits>
 
 #include "base/android/jni_android.h"
 #include "base/android/jni_string.h"
@@ -13,313 +12,134 @@
 #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/banners/app_banner_settings_helper.h"
-#include "chrome/browser/favicon/favicon_service_factory.h"
-#include "chrome/browser/manifest/manifest_icon_selector.h"
-#include "chrome/browser/profiles/profile.h"
-#include "chrome/common/chrome_constants.h"
-#include "chrome/common/render_messages.h"
-#include "chrome/common/web_application_info.h"
-#include "components/dom_distiller/core/url_utils.h"
-#include "components/favicon/core/favicon_service.h"
-#include "content/public/browser/user_metrics.h"
+#include "content/public/browser/browser_thread.h"
 #include "content/public/browser/web_contents.h"
-#include "content/public/browser/web_contents_observer.h"
-#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 "third_party/WebKit/public/platform/WebScreenOrientationLockType.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, jobject java_web_contents) {
   content::WebContents* web_contents =
       content::WebContents::FromJavaWebContents(java_web_contents);
   ShortcutHelper* shortcut_helper = new ShortcutHelper(env, obj, web_contents);
-  shortcut_helper->Initialize();
-
   return reinterpret_cast<intptr_t>(shortcut_helper);
 }
 
 ShortcutHelper::ShortcutHelper(JNIEnv* env,
                                jobject obj,
                                content::WebContents* web_contents)
-    : WebContentsObserver(web_contents),
-      java_ref_(env, obj),
-      shortcut_info_(dom_distiller::url_utils::GetOriginalUrlFromDistillerUrl(
-                     web_contents->GetURL())),
-      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) {
-}
-
-void ShortcutHelper::Initialize() {
-  // Send a message to the renderer to retrieve information about the page.
-  Send(new ChromeViewMsg_GetWebApplicationInfo(routing_id()));
+    : add_shortcut_pending_(false),
+      data_fetcher_(new ShortcutDataFetcher(web_contents, this)) {
+  java_ref_.Reset(env, obj);
 }
 
 ShortcutHelper::~ShortcutHelper() {
+  data_fetcher_->set_weak_observer(nullptr);
+  data_fetcher_ = nullptr;
 }
 
-void ShortcutHelper::OnDidGetWebApplicationInfo(
-    const WebApplicationInfo& received_web_app_info) {
-  // Sanitize received_web_app_info.
-  WebApplicationInfo web_app_info = received_web_app_info;
-  web_app_info.title =
-      web_app_info.title.substr(0, chrome::kMaxMetaTagAttributeLength);
-  web_app_info.description =
-      web_app_info.description.substr(0, chrome::kMaxMetaTagAttributeLength);
-
-  shortcut_info_.title = web_app_info.title.empty() ? web_contents()->GetTitle()
-                                                    : web_app_info.title;
-
-  if (web_app_info.mobile_capable == WebApplicationInfo::MOBILE_CAPABLE ||
-      web_app_info.mobile_capable == WebApplicationInfo::MOBILE_CAPABLE_APPLE) {
-    shortcut_info_.display = content::Manifest::DISPLAY_MODE_STANDALONE;
-  }
-
-  // Record what type of shortcut was added by the user.
-  switch (web_app_info.mobile_capable) {
-    case WebApplicationInfo::MOBILE_CAPABLE:
-      content::RecordAction(
-          base::UserMetricsAction("webapps.AddShortcut.AppShortcut"));
-      break;
-    case WebApplicationInfo::MOBILE_CAPABLE_APPLE:
-      content::RecordAction(
-          base::UserMetricsAction("webapps.AddShortcut.AppShortcutApple"));
-      break;
-    case WebApplicationInfo::MOBILE_CAPABLE_UNSPECIFIED:
-      content::RecordAction(
-          base::UserMetricsAction("webapps.AddShortcut.Bookmark"));
-      break;
-  }
-
-  web_contents()->GetManifest(base::Bind(&ShortcutHelper::OnDidGetManifest,
-                                         weak_ptr_factory_.GetWeakPtr()));
-}
-
-void ShortcutHelper::OnDidGetManifest(const content::Manifest& manifest) {
-  if (!manifest.IsEmpty()) {
-      content::RecordAction(
-          base::UserMetricsAction("webapps.AddShortcut.Manifest"));
-  }
-
-  shortcut_info_.UpdateFromManifest(manifest);
-
-  GURL icon_src = ManifestIconSelector::FindBestMatchingIcon(
-      manifest.icons,
-      kPreferredIconSizeInDp,
-      gfx::Screen::GetScreenFor(web_contents()->GetNativeView()));
-  if (icon_src.is_valid()) {
-    web_contents()->DownloadImage(icon_src,
-                                  false,
-                                  preferred_icon_size_in_px_,
-                                  false,
-                                  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
-  // these data are available.
+void ShortcutHelper::OnTitleAvailable(const base::string16& title) {
   JNIEnv* env = base::android::AttachCurrentThread();
-  ScopedJavaLocalRef<jobject> j_obj = java_ref_.get(env);
   ScopedJavaLocalRef<jstring> j_title =
-      base::android::ConvertUTF16ToJavaString(env, shortcut_info_.title);
-
-  Java_ShortcutHelper_onInitialized(env, j_obj.obj(), j_title.obj());
+      base::android::ConvertUTF16ToJavaString(env, title);
+  Java_ShortcutHelper_onTitleAvailable(env,
+                                       java_ref_.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;
-  }
+void ShortcutHelper::OnDataAvailable(const ShortcutInfo& info,
+                                     const SkBitmap& icon) {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  ScopedJavaLocalRef<jobject> java_bitmap;
+  if (icon.getSize())
+    java_bitmap = gfx::ConvertToJavaBitmap(&icon);
 
-  // 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;
-  }
+  Java_ShortcutHelper_onIconAvailable(env,
+                                      java_ref_.obj(),
+                                      java_bitmap.obj());
 
-  manifest_icon_ = bitmaps[preferred_bitmap_index];
-  manifest_icon_status_ = MANIFEST_ICON_STATUS_DONE;
-
-  if (add_shortcut_requested_)
-    AddShortcutUsingManifestIcon();
+  if (add_shortcut_pending_)
+    AddShortcut(info, icon);
 }
 
-void ShortcutHelper::TearDown(JNIEnv*, jobject) {
-  Destroy();
-}
-
-void ShortcutHelper::Destroy() {
+void ShortcutHelper::Destroy(JNIEnv* env, jobject obj) {
   delete this;
 }
 
-void ShortcutHelper::AddShortcut(
-    JNIEnv* env,
-    jobject obj,
-    jstring jtitle,
-    jint launcher_large_icon_size) {
-  add_shortcut_requested_ = true;
+SkBitmap ShortcutHelper::FinalizeLauncherIcon(const SkBitmap& bitmap,
+                                              const GURL& url) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
+
+  // Determine a single color to use for the favicon if the favicon that is
+  // returned it is too low quality.
+  SkColor color = color_utils::CalculateKMeanColorOfBitmap(bitmap);
+  int dominant_red = SkColorGetR(color);
+  int dominant_green = SkColorGetG(color);
+  int dominant_blue = SkColorGetB(color);
+
+  // Make the icon acceptable for the Android launcher.
+  JNIEnv* env = base::android::AttachCurrentThread();
+  ScopedJavaLocalRef<jstring> java_url =
+      base::android::ConvertUTF8ToJavaString(env, url.spec());
+  ScopedJavaLocalRef<jobject> java_bitmap;
+  if (bitmap.getSize())
+    java_bitmap = gfx::ConvertToJavaBitmap(&bitmap);
+
+  base::android::ScopedJavaLocalRef<jobject> ref =
+      Java_ShortcutHelper_finalizeLauncherIcon(env,
+                                               java_url.obj(),
+                                               java_bitmap.obj(),
+                                               dominant_red,
+                                               dominant_green,
+                                               dominant_blue);
+  return gfx::CreateSkBitmapFromJavaBitmap(gfx::JavaBitmap(ref.obj()));
+}
+
+void ShortcutHelper::AddShortcut(JNIEnv* env, jobject obj, jstring jtitle) {
+  add_shortcut_pending_ = true;
 
   base::string16 title = base::android::ConvertJavaStringToUTF16(env, jtitle);
   if (!title.empty())
-    shortcut_info_.title = title;
+    data_fetcher_->shortcut_info().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;
+  if (data_fetcher_->is_ready()) {
+    // If the fetcher isn't ready yet, the shortcut will be added when it is
+    // via OnDataAvailable();
+    AddShortcut(data_fetcher_->shortcut_info(), data_fetcher_->shortcut_icon());
   }
 }
 
-void ShortcutHelper::AddShortcutUsingManifestIcon() {
+void ShortcutHelper::AddShortcut(const ShortcutInfo& info,
+                                 const SkBitmap& icon) {
+  DCHECK(add_shortcut_pending_);
+  if (!add_shortcut_pending_)
+    return;
+  add_shortcut_pending_ = false;
+
   RecordAddToHomescreen();
 
-  // Stop observing so we don't get destroyed while doing the last steps.
-  Observe(NULL);
-
-  base::WorkerPool::PostTask(
+  content::BrowserThread::PostTask(
+      content::BrowserThread::IO,
       FROM_HERE,
       base::Bind(&ShortcutHelper::AddShortcutInBackgroundWithSkBitmap,
-                 shortcut_info_,
-                 manifest_icon_,
-                 true),
-      true);
-
-  Destroy();
-}
-
-void ShortcutHelper::AddShortcutUsingFavicon() {
-  RecordAddToHomescreen();
-
-  Profile* profile =
-      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
-
-  // Grab the best, largest icon we can find to represent this bookmark.
-  // TODO(dfalcantara): Try combining with the new BookmarksHandler once its
-  //                    rewrite is further along.
-  std::vector<int> icon_types;
-  icon_types.push_back(favicon_base::FAVICON);
-  icon_types.push_back(favicon_base::TOUCH_PRECOMPOSED_ICON |
-                       favicon_base::TOUCH_ICON);
-  favicon::FaviconService* favicon_service =
-      FaviconServiceFactory::GetForProfile(profile,
-                                           ServiceAccessType::EXPLICIT_ACCESS);
-
-  // 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 = preferred_icon_size_in_px_ - 1;
-  favicon_service->GetLargestRawFaviconForPageURL(
-      shortcut_info_.url,
-      icon_types,
-      threshold_to_get_any_largest_icon,
-      base::Bind(&ShortcutHelper::OnDidGetFavicon,
-                 base::Unretained(this)),
-      &cancelable_task_tracker_);
-}
-
-void ShortcutHelper::OnDidGetFavicon(
-    const favicon_base::FaviconRawBitmapResult& 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::AddShortcutInBackgroundWithRawBitmap,
-                 shortcut_info_,
-                 bitmap_result),
-      true);
-
-  Destroy();
-}
-
-bool ShortcutHelper::OnMessageReceived(const IPC::Message& message) {
-  bool handled = true;
-
-  IPC_BEGIN_MESSAGE_MAP(ShortcutHelper, message)
-    IPC_MESSAGE_HANDLER(ChromeViewHostMsg_DidGetWebApplicationInfo,
-                        OnDidGetWebApplicationInfo)
-    IPC_MESSAGE_UNHANDLED(handled = false)
-  IPC_END_MESSAGE_MAP()
-
-  return handled;
-}
-
-void ShortcutHelper::WebContentsDestroyed() {
-  Destroy();
+                 info,
+                 icon));
 }
 
 bool ShortcutHelper::RegisterShortcutHelper(JNIEnv* env) {
   return RegisterNativesImpl(env);
 }
 
-void ShortcutHelper::AddShortcutInBackgroundWithRawBitmap(
-    const ShortcutInfo& info,
-    const favicon_base::FaviconRawBitmapResult& bitmap_result) {
-  DCHECK(base::WorkerPool::RunsTasksOnCurrentThread());
-
-  SkBitmap icon_bitmap;
-  if (bitmap_result.is_valid()) {
-    gfx::PNGCodec::Decode(bitmap_result.bitmap_data->front(),
-                          bitmap_result.bitmap_data->size(),
-                          &icon_bitmap);
-  }
-
-  AddShortcutInBackgroundWithSkBitmap(info, icon_bitmap, true);
-}
-
+// static
 void ShortcutHelper::AddShortcutInBackgroundWithSkBitmap(
     const ShortcutInfo& info,
-    const SkBitmap& icon_bitmap,
-    const bool return_to_homescreen) {
-  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);
+    const SkBitmap& icon_bitmap) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
 
   // Send the data to the Java side to create the shortcut.
   JNIEnv* env = base::android::AttachCurrentThread();
@@ -337,19 +157,20 @@
       java_url.obj(),
       java_title.obj(),
       java_bitmap.obj(),
-      r_value,
-      g_value,
-      b_value,
       info.display == content::Manifest::DISPLAY_MODE_STANDALONE,
-      info.orientation,
-      return_to_homescreen);
+      info.orientation);
 }
 
 void ShortcutHelper::RecordAddToHomescreen() {
   // Record that the shortcut has been added, so no banners will be shown
   // for this app.
+  content::WebContents* web_contents = data_fetcher_->web_contents();
+  if (!web_contents)
+    return;
+
   AppBannerSettingsHelper::RecordBannerEvent(
-      web_contents(), web_contents()->GetURL(), shortcut_info_.url.spec(),
+      web_contents, web_contents->GetURL(),
+      data_fetcher_->shortcut_info().url.spec(),
       AppBannerSettingsHelper::APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN,
       base::Time::Now());
 }