blob: e8619b6190739434a0dac8c698ed3bf3be9137e2 [file] [log] [blame]
// Copyright 2015 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/webapps/add_to_homescreen_data_fetcher.h"
#include <utility>
#include <vector>
#include "base/android/build_info.h"
#include "base/bind.h"
#include "base/location.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/sequenced_task_runner.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chrome/browser/android/shortcut_helper.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_constants.h"
#include "components/dom_distiller/core/url_utils.h"
#include "components/favicon/core/favicon_service.h"
#include "components/favicon_base/favicon_types.h"
#include "components/webapps/android/webapps_icon_utils.h"
#include "components/webapps/android/webapps_utils.h"
#include "components/webapps/installable/installable_manager.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/manifest/manifest.h"
#include "third_party/blink/public/common/manifest/manifest_icon_selector.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/favicon_size.h"
#include "url/gurl.h"
namespace webapps {
namespace {
// Looks up the original, online, visible URL of |web_contents|. The current
// visible URL may be a distilled article which is not appropriate for a home
// screen shortcut.
GURL GetShortcutUrl(content::WebContents* web_contents) {
return dom_distiller::url_utils::GetOriginalUrlFromDistillerUrl(
web_contents->GetVisibleURL());
}
InstallableParams ParamsToPerformManifestAndIconFetch() {
InstallableParams params;
params.valid_primary_icon = true;
params.prefer_maskable_icon =
WebappsIconUtils::DoesAndroidSupportMaskableIcons();
params.wait_for_worker = true;
return params;
}
InstallableParams ParamsToPerformInstallableCheck() {
InstallableParams params;
params.check_eligibility = true;
params.valid_manifest = true;
params.has_worker = true;
params.valid_primary_icon = true;
params.prefer_maskable_icon =
WebappsIconUtils::DoesAndroidSupportMaskableIcons();
params.wait_for_worker = true;
return params;
}
// Creates a launcher icon from |icon|. |start_url| is used to generate the icon
// if |icon| is empty or is not large enough. When complete, posts |callback| on
// |ui_thread_task_runner| binding:
// - the generated icon
// - whether |icon| was used in generating the launcher icon
void CreateLauncherIconInBackground(
const GURL& start_url,
const SkBitmap& icon,
bool maskable,
scoped_refptr<base::SequencedTaskRunner> ui_thread_task_runner,
base::OnceCallback<void(const SkBitmap&, bool)> callback) {
bool is_generated = false;
SkBitmap primary_icon = WebappsIconUtils::FinalizeLauncherIconInBackground(
icon, maskable, start_url, &is_generated);
ui_thread_task_runner->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), primary_icon, is_generated));
}
// Creates a launcher icon from |bitmap_result|. |start_url| is used to
// generate the icon if there is no bitmap in |bitmap_result| or the bitmap is
// not large enough.
void CreateLauncherIconFromFaviconInBackground(
const GURL& start_url,
const favicon_base::FaviconRawBitmapResult& bitmap_result,
scoped_refptr<base::SequencedTaskRunner> ui_thread_task_runner,
base::OnceCallback<void(const SkBitmap&, bool)> callback) {
SkBitmap decoded;
if (bitmap_result.is_valid()) {
base::AssertLongCPUWorkAllowed();
gfx::PNGCodec::Decode(bitmap_result.bitmap_data->front(),
bitmap_result.bitmap_data->size(), &decoded);
}
CreateLauncherIconInBackground(start_url, decoded,
/*maskable=*/false, ui_thread_task_runner,
std::move(callback));
}
void RecordAddToHomescreenDialogDuration(base::TimeDelta duration) {
UMA_HISTOGRAM_TIMES("Webapp.AddToHomescreenDialog.Timeout", duration);
}
} // namespace
AddToHomescreenDataFetcher::AddToHomescreenDataFetcher(
content::WebContents* web_contents,
int data_timeout_ms,
Observer* observer)
: content::WebContentsObserver(web_contents),
installable_manager_(InstallableManager::FromWebContents(web_contents)),
observer_(observer),
shortcut_info_(GetShortcutUrl(web_contents)),
has_maskable_primary_icon_(false),
data_timeout_ms_(base::TimeDelta::FromMilliseconds(data_timeout_ms)),
is_waiting_for_manifest_(true) {
DCHECK(shortcut_info_.url.is_valid());
// Send a message to the renderer to retrieve information about the page.
mojo::AssociatedRemote<chrome::mojom::ChromeRenderFrame> chrome_render_frame;
web_contents->GetMainFrame()->GetRemoteAssociatedInterfaces()->GetInterface(
&chrome_render_frame);
// Bind the InterfacePtr into the callback so that it's kept alive until
// there's either a connection error or a response.
auto* web_page_metadata_proxy = chrome_render_frame.get();
web_page_metadata_proxy->GetWebPageMetadata(base::BindOnce(
&AddToHomescreenDataFetcher::OnDidGetWebPageMetadata,
weak_ptr_factory_.GetWeakPtr(), base::Passed(&chrome_render_frame)));
}
AddToHomescreenDataFetcher::~AddToHomescreenDataFetcher() = default;
void AddToHomescreenDataFetcher::OnDidGetWebPageMetadata(
mojo::AssociatedRemote<chrome::mojom::ChromeRenderFrame>
chrome_render_frame,
chrome::mojom::WebPageMetadataPtr web_page_metadata) {
if (!web_contents())
return;
// Note, the title should have already been clipped on the renderer side.
// TODO(https://crbug.com/673422): Would be nice if this constraint could be
// specified directly in the mojom file and enforced automatically.
if (web_page_metadata->application_name.size() >
chrome::kMaxMetaTagAttributeLength) {
mojo::ReportBadMessage("application_name is too long");
return;
}
// Set the user-editable title to be the page's title.
shortcut_info_.user_title = web_page_metadata->application_name.empty()
? web_contents()->GetTitle()
: web_page_metadata->application_name;
shortcut_info_.short_name = shortcut_info_.user_title;
shortcut_info_.name = shortcut_info_.user_title;
if (web_page_metadata->mobile_capable ==
chrome::mojom::WebPageMobileCapable::ENABLED ||
web_page_metadata->mobile_capable ==
chrome::mojom::WebPageMobileCapable::ENABLED_APPLE) {
shortcut_info_.display = blink::mojom::DisplayMode::kStandalone;
shortcut_info_.UpdateSource(
ShortcutInfo::SOURCE_ADD_TO_HOMESCREEN_STANDALONE);
}
// Record what type of shortcut was added by the user.
switch (web_page_metadata->mobile_capable) {
case chrome::mojom::WebPageMobileCapable::ENABLED:
base::RecordAction(
base::UserMetricsAction("webapps.AddShortcut.AppShortcut"));
break;
case chrome::mojom::WebPageMobileCapable::ENABLED_APPLE:
base::RecordAction(
base::UserMetricsAction("webapps.AddShortcut.AppShortcutApple"));
break;
case chrome::mojom::WebPageMobileCapable::UNSPECIFIED:
base::RecordAction(
base::UserMetricsAction("webapps.AddShortcut.Bookmark"));
break;
}
// Kick off a timeout for downloading web app manifest data. If we haven't
// finished within the timeout, fall back to using any fetched icon, or at
// worst, a dynamically-generated launcher icon.
data_timeout_timer_.Start(
FROM_HERE, data_timeout_ms_,
base::BindOnce(&AddToHomescreenDataFetcher::OnDataTimedout,
weak_ptr_factory_.GetWeakPtr()));
start_time_ = base::TimeTicks::Now();
installable_manager_->GetData(
ParamsToPerformManifestAndIconFetch(),
base::BindOnce(&AddToHomescreenDataFetcher::OnDidGetManifestAndIcons,
weak_ptr_factory_.GetWeakPtr()));
}
void AddToHomescreenDataFetcher::StopTimer() {
data_timeout_timer_.Stop();
RecordAddToHomescreenDialogDuration(base::TimeTicks::Now() - start_time_);
}
void AddToHomescreenDataFetcher::OnDataTimedout() {
RecordAddToHomescreenDialogDuration(data_timeout_ms_);
weak_ptr_factory_.InvalidateWeakPtrs();
if (!web_contents())
return;
observer_->OnUserTitleAvailable(shortcut_info_.user_title, shortcut_info_.url,
/*is_webapk_compatible=*/false);
CreateIconForView(raw_primary_icon_, /*use_for_launcher=*/true);
}
void AddToHomescreenDataFetcher::OnDidGetManifestAndIcons(
const InstallableData& data) {
if (!web_contents())
return;
is_waiting_for_manifest_ = false;
if (!data.manifest->IsEmpty()) {
base::RecordAction(base::UserMetricsAction("webapps.AddShortcut.Manifest"));
shortcut_info_.UpdateFromManifest(*data.manifest);
shortcut_info_.manifest_url = data.manifest_url;
}
// Do this after updating from the manifest for the case where a site has
// a manifest with name and standalone specified, but no icons.
if (data.manifest->IsEmpty() || !data.primary_icon) {
observer_->OnUserTitleAvailable(shortcut_info_.user_title,
shortcut_info_.url,
/*is_webapk_compatible=*/false);
StopTimer();
FetchFavicon();
return;
}
raw_primary_icon_ = *data.primary_icon;
has_maskable_primary_icon_ = data.has_maskable_primary_icon;
shortcut_info_.best_primary_icon_url = data.primary_icon_url;
// Save the splash screen URL for the later download.
shortcut_info_.ideal_splash_image_size_in_px =
WebappsIconUtils::GetIdealSplashImageSizeInPx();
shortcut_info_.minimum_splash_image_size_in_px =
WebappsIconUtils::GetMinimumSplashImageSizeInPx();
shortcut_info_.splash_image_url =
blink::ManifestIconSelector::FindBestMatchingSquareIcon(
data.manifest->icons, shortcut_info_.ideal_splash_image_size_in_px,
shortcut_info_.minimum_splash_image_size_in_px,
blink::mojom::ManifestImageResource_Purpose::ANY);
installable_manager_->GetData(
ParamsToPerformInstallableCheck(),
base::BindOnce(&AddToHomescreenDataFetcher::OnDidPerformInstallableCheck,
weak_ptr_factory_.GetWeakPtr()));
}
void AddToHomescreenDataFetcher::OnDidPerformInstallableCheck(
const InstallableData& data) {
StopTimer();
if (!web_contents())
return;
bool webapk_compatible =
(data.NoBlockingErrors() && data.valid_manifest && data.has_worker &&
WebappsUtils::AreWebManifestUrlsWebApkCompatible(*data.manifest));
observer_->OnUserTitleAvailable(
webapk_compatible ? shortcut_info_.name : shortcut_info_.user_title,
shortcut_info_.url, webapk_compatible);
bool should_use_created_icon_for_launcher = true;
if (webapk_compatible) {
// WebAPKs should always use the raw icon for the launcher whether or not
// that icon is maskable.
should_use_created_icon_for_launcher = false;
primary_icon_ = raw_primary_icon_;
shortcut_info_.UpdateSource(ShortcutInfo::SOURCE_ADD_TO_HOMESCREEN_PWA);
if (!has_maskable_primary_icon_) {
// We can skip creating an icon for the view because the raw icon is
// sufficient when WebAPK-compatible and the icon is non-maskable.
OnIconCreated(should_use_created_icon_for_launcher, raw_primary_icon_,
/*is_icon_generated=*/false);
return;
}
}
CreateIconForView(raw_primary_icon_, should_use_created_icon_for_launcher);
}
void AddToHomescreenDataFetcher::FetchFavicon() {
if (!web_contents())
return;
// Grab the best, largest icon we can find to represent this bookmark.
std::vector<favicon_base::IconTypeSet> icon_types = {
{favicon_base::IconType::kWebManifestIcon},
{favicon_base::IconType::kFavicon},
{favicon_base::IconType::kTouchPrecomposedIcon,
favicon_base::IconType::kTouchIcon}};
favicon::FaviconService* favicon_service =
FaviconServiceFactory::GetForProfile(
Profile::FromBrowserContext(web_contents()->GetBrowserContext()),
ServiceAccessType::EXPLICIT_ACCESS);
// Using favicon if its size is not smaller than platform required size,
// otherwise using the largest icon among all available icons.
int threshold_to_get_any_largest_icon =
WebappsIconUtils::GetIdealHomescreenIconSizeInPx() - 1;
favicon_service->GetLargestRawFaviconForPageURL(
shortcut_info_.url, icon_types, threshold_to_get_any_largest_icon,
base::BindOnce(&AddToHomescreenDataFetcher::OnFaviconFetched,
weak_ptr_factory_.GetWeakPtr()),
&favicon_task_tracker_);
}
void AddToHomescreenDataFetcher::OnFaviconFetched(
const favicon_base::FaviconRawBitmapResult& bitmap_result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!web_contents())
return;
shortcut_info_.best_primary_icon_url = bitmap_result.icon_url;
// The user is waiting for the icon to be processed before they can proceed
// with add to homescreen. But if we shut down, there's no point starting the
// image processing. Use USER_VISIBLE with MayBlock and SKIP_ON_SHUTDOWN.
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(&CreateLauncherIconFromFaviconInBackground,
shortcut_info_.url, bitmap_result,
base::ThreadTaskRunnerHandle::Get(),
base::BindOnce(&AddToHomescreenDataFetcher::OnIconCreated,
weak_ptr_factory_.GetWeakPtr(),
/*use_for_launcher=*/true)));
}
void AddToHomescreenDataFetcher::CreateIconForView(const SkBitmap& base_icon,
bool use_for_launcher) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// The user is waiting for the icon to be processed before they can proceed
// with add to homescreen. But if we shut down, there's no point starting the
// image processing. Use USER_VISIBLE with MayBlock and SKIP_ON_SHUTDOWN.
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(
&CreateLauncherIconInBackground, shortcut_info_.url, base_icon,
has_maskable_primary_icon_, base::ThreadTaskRunnerHandle::Get(),
base::BindOnce(&AddToHomescreenDataFetcher::OnIconCreated,
weak_ptr_factory_.GetWeakPtr(), use_for_launcher)));
}
void AddToHomescreenDataFetcher::OnIconCreated(bool use_for_launcher,
const SkBitmap& icon_for_view,
bool is_icon_generated) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!web_contents())
return;
if (use_for_launcher)
primary_icon_ = icon_for_view;
if (is_icon_generated)
shortcut_info_.best_primary_icon_url = GURL();
observer_->OnDataAvailable(shortcut_info_, icon_for_view);
}
} // namespace webapps