blob: 6768e28c32a21177b6a13a5d8c1eb6616507a678 [file] [log] [blame]
// Copyright 2016 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 <memory>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/memory/ref_counted.h"
#include "base/optional.h"
#include "base/run_loop.h"
#include "base/strings/string16.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/flags/android/chrome_feature_list.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/installable/installable_manager.h"
#include "chrome/browser/installable/installable_metrics.h"
#include "chrome/common/web_application_info.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/manifest/manifest.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "url/gurl.h"
namespace {
const char* kWebApplicationInfoTitle = "Meta Title";
const char* kDefaultManifestUrl = "https://www.example.com/manifest.json";
const char* kDefaultIconUrl = "https://www.example.com/icon.png";
const char* kDefaultManifestName = "Default Name";
const char* kDefaultManifestShortName = "Default Short Name";
const char* kDefaultStartUrl = "https://www.example.com/index.html";
const blink::mojom::DisplayMode kDefaultManifestDisplayMode =
blink::mojom::DisplayMode::kStandalone;
const int kIconSizePx = 144;
// Tracks which of the AddToHomescreenDataFetcher::Observer methods have been
// called.
class ObserverWaiter : public AddToHomescreenDataFetcher::Observer {
public:
ObserverWaiter()
: is_webapk_compatible_(false),
title_available_(false),
data_available_(false) {}
~ObserverWaiter() override {}
// Waits till the OnDataAvailable() callback is called.
void WaitForDataAvailable() {
if (data_available_)
return;
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
run_loop.Run();
}
void OnUserTitleAvailable(const base::string16& title,
const GURL& url,
bool is_webapk_compatible) override {
// This should only be called once.
EXPECT_FALSE(title_available_);
EXPECT_FALSE(data_available_);
title_available_ = true;
title_ = title;
is_webapk_compatible_ = is_webapk_compatible;
}
void OnDataAvailable(const ShortcutInfo& info,
const SkBitmap& primary_icon) override {
// This should only be called once.
EXPECT_FALSE(data_available_);
EXPECT_TRUE(title_available_);
data_available_ = true;
if (!quit_closure_.is_null())
quit_closure_.Run();
}
base::string16 title() const { return title_; }
bool is_webapk_compatible() const { return is_webapk_compatible_; }
bool title_available() const { return title_available_; }
private:
base::string16 title_;
bool is_webapk_compatible_;
bool title_available_;
bool data_available_;
base::Closure quit_closure_;
DISALLOW_COPY_AND_ASSIGN(ObserverWaiter);
};
// Builds WebAPK compatible blink::Manifest.
blink::Manifest BuildDefaultManifest() {
blink::Manifest manifest;
manifest.name = base::ASCIIToUTF16(kDefaultManifestName);
manifest.short_name = base::ASCIIToUTF16(kDefaultManifestShortName);
manifest.start_url = GURL(kDefaultStartUrl);
manifest.display = kDefaultManifestDisplayMode;
blink::Manifest::ImageResource primary_icon;
primary_icon.type = base::ASCIIToUTF16("image/png");
primary_icon.sizes.push_back(gfx::Size(144, 144));
primary_icon.purpose.push_back(
blink::mojom::ManifestImageResource_Purpose::ANY);
primary_icon.src = GURL(kDefaultIconUrl);
manifest.icons.push_back(primary_icon);
return manifest;
}
} // anonymous namespace
class TestInstallableManager : public InstallableManager {
public:
explicit TestInstallableManager(content::WebContents* web_contents)
: InstallableManager(web_contents) {}
// Mock out the GetData API so we can control exactly what is returned to the
// data fetcher.
void GetData(const InstallableParams& params,
InstallableCallback callback) override {
InstallableStatusCode code = NO_ERROR_DETECTED;
bool is_installable = is_installable_;
if (params.valid_primary_icon && !primary_icon_) {
code = NO_ACCEPTABLE_ICON;
is_installable = false;
} else if (params.valid_manifest && params.has_worker) {
if (!IsManifestValidForWebApp(manifest_,
true /* check_webapp_manifest_display */,
false /* prefer_maskable_icon */)) {
code = valid_manifest_->errors.at(0);
is_installable = false;
} else if (!is_installable_) {
code = NOT_OFFLINE_CAPABLE;
is_installable = false;
}
}
if (should_manifest_time_out_ ||
(params.valid_manifest && params.has_worker &&
should_installable_time_out_)) {
return;
}
std::vector<InstallableStatusCode> errors;
if (code != NO_ERROR_DETECTED)
errors.push_back(code);
std::move(callback).Run(
{std::move(errors), GURL(kDefaultManifestUrl), &manifest_,
params.valid_primary_icon ? primary_icon_url_ : GURL(),
params.valid_primary_icon ? primary_icon_.get() : nullptr,
params.prefer_maskable_icon, GURL() /* splash_icon_url */,
nullptr /* splash_icon */,
params.valid_manifest ? is_installable : false,
params.has_worker ? is_installable : false});
}
void SetInstallable(bool is_installable) { is_installable_ = is_installable; }
void SetManifest(const blink::Manifest& manifest) {
manifest_ = manifest;
if (!manifest.icons.empty()) {
primary_icon_url_ = manifest_.icons[0].src;
primary_icon_.reset(
new SkBitmap(gfx::test::CreateBitmap(kIconSizePx, kIconSizePx)));
}
}
void SetShouldManifestTimeOut(bool should_time_out) {
should_manifest_time_out_ = should_time_out;
}
void SetShouldInstallableTimeOut(bool should_time_out) {
should_installable_time_out_ = should_time_out;
}
private:
blink::Manifest manifest_;
GURL primary_icon_url_;
std::unique_ptr<SkBitmap> primary_icon_;
bool is_installable_ = true;
bool should_manifest_time_out_ = false;
bool should_installable_time_out_ = false;
};
// Tests AddToHomescreenDataFetcher. These tests should be browser tests but
// Android does not support browser tests yet (crbug.com/611756).
class AddToHomescreenDataFetcherTest : public ChromeRenderViewHostTestHarness {
public:
AddToHomescreenDataFetcherTest() {}
~AddToHomescreenDataFetcherTest() override {}
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
// Manually inject the TestInstallableManager as a "InstallableManager"
// WebContentsUserData. We can't directly call ::CreateForWebContents due to
// typing issues since TestInstallableManager doesn't directly inherit from
// WebContentsUserData.
web_contents()->SetUserData(
TestInstallableManager::UserDataKey(),
base::WrapUnique(new TestInstallableManager(web_contents())));
installable_manager_ = static_cast<TestInstallableManager*>(
web_contents()->GetUserData(TestInstallableManager::UserDataKey()));
NavigateAndCommit(GURL(kDefaultStartUrl));
}
TestingProfile::TestingFactories GetTestingFactories() const override {
return {{HistoryServiceFactory::GetInstance(),
HistoryServiceFactory::GetDefaultFactory()},
{FaviconServiceFactory::GetInstance(),
FaviconServiceFactory::GetDefaultFactory()}};
}
std::unique_ptr<AddToHomescreenDataFetcher> BuildFetcher(
AddToHomescreenDataFetcher::Observer* observer) {
return std::make_unique<AddToHomescreenDataFetcher>(web_contents(), 500,
observer);
}
void RunFetcher(AddToHomescreenDataFetcher* fetcher,
ObserverWaiter& waiter,
const char* expected_user_title,
const char* expected_name,
blink::mojom::DisplayMode display_mode,
bool is_webapk_compatible) {
WebApplicationInfo web_application_info;
web_application_info.title = base::UTF8ToUTF16(kWebApplicationInfoTitle);
fetcher->OnDidGetWebApplicationInfo(
mojo::AssociatedRemote<chrome::mojom::ChromeRenderFrame>(),
web_application_info);
waiter.WaitForDataAvailable();
EXPECT_EQ(is_webapk_compatible, waiter.is_webapk_compatible());
EXPECT_TRUE(waiter.title_available());
if (is_webapk_compatible)
EXPECT_TRUE(base::EqualsASCII(waiter.title(), expected_name));
else
EXPECT_TRUE(base::EqualsASCII(waiter.title(), expected_user_title));
EXPECT_TRUE(base::EqualsASCII(fetcher->shortcut_info().user_title,
expected_user_title));
EXPECT_EQ(display_mode, fetcher->shortcut_info().display);
}
void RunFetcher(AddToHomescreenDataFetcher* fetcher,
ObserverWaiter& waiter,
const char* expected_title,
blink::mojom::DisplayMode display_mode,
bool is_webapk_compatible) {
RunFetcher(fetcher, waiter, expected_title, expected_title, display_mode,
is_webapk_compatible);
}
void CheckHistograms(base::HistogramTester& histograms) {
histograms.ExpectTotalCount("Webapp.AddToHomescreenDialog.Timeout", 1);
}
void SetManifest(const blink::Manifest& manifest) {
installable_manager_->SetManifest(manifest);
}
void SetInstallable(bool is_installable) {
installable_manager_->SetInstallable(is_installable);
}
void SetShouldManifestTimeOut(bool should_time_out) {
installable_manager_->SetShouldManifestTimeOut(should_time_out);
}
void SetShouldInstallableTimeOut(bool should_time_out) {
installable_manager_->SetShouldInstallableTimeOut(should_time_out);
}
private:
TestInstallableManager* installable_manager_;
DISALLOW_COPY_AND_ASSIGN(AddToHomescreenDataFetcherTest);
};
TEST_F(AddToHomescreenDataFetcherTest, EmptyManifest) {
// Check that an empty manifest has the appropriate methods run.
base::HistogramTester histograms;
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kWebApplicationInfoTitle,
blink::mojom::DisplayMode::kBrowser, false);
CheckHistograms(histograms);
}
TEST_F(AddToHomescreenDataFetcherTest, NoIconManifest) {
// Test a manifest with no icons. This should use the short name and have
// a generated icon (empty icon url).
blink::Manifest manifest = BuildDefaultManifest();
manifest.icons.clear();
SetManifest(manifest);
base::HistogramTester histograms;
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kDefaultManifestShortName,
blink::mojom::DisplayMode::kStandalone, false);
CheckHistograms(histograms);
EXPECT_TRUE(fetcher->shortcut_info().best_primary_icon_url.is_empty());
EXPECT_TRUE(fetcher->shortcut_info().splash_image_url.is_empty());
}
// Check that the AddToHomescreenDataFetcher::Observer methods are called
// if the first call to InstallableManager::GetData() times out. This should
// fall back to the metadata title and have a non-empty icon (taken from the
// favicon).
TEST_F(AddToHomescreenDataFetcherTest, ManifestFetchTimesOutPwa) {
SetShouldManifestTimeOut(true);
SetManifest(BuildDefaultManifest());
// Check a site where InstallableManager finishes working after the time out
// and determines PWA-ness. This is only relevant when checking WebAPK
// compatibility.
base::HistogramTester histograms;
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kWebApplicationInfoTitle,
blink::mojom::DisplayMode::kBrowser, false);
CheckHistograms(histograms);
EXPECT_FALSE(fetcher->primary_icon().drawsNothing());
EXPECT_TRUE(fetcher->shortcut_info().best_primary_icon_url.is_empty());
}
TEST_F(AddToHomescreenDataFetcherTest, ManifestFetchTimesOutNonPwa) {
SetShouldManifestTimeOut(true);
SetManifest(BuildDefaultManifest());
SetInstallable(false);
// Check where InstallableManager finishes working after the time out and
// determines non-PWA-ness.
base::HistogramTester histograms;
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kWebApplicationInfoTitle,
blink::mojom::DisplayMode::kBrowser, false);
CheckHistograms(histograms);
EXPECT_FALSE(fetcher->primary_icon().drawsNothing());
EXPECT_TRUE(fetcher->shortcut_info().best_primary_icon_url.is_empty());
}
TEST_F(AddToHomescreenDataFetcherTest, ManifestFetchTimesOutUnknown) {
SetShouldManifestTimeOut(true);
SetShouldInstallableTimeOut(true);
SetManifest(BuildDefaultManifest());
// Check where InstallableManager doesn't finish working after the time out.
base::HistogramTester histograms;
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kWebApplicationInfoTitle,
blink::mojom::DisplayMode::kBrowser, false);
NavigateAndCommit(GURL("about:blank"));
CheckHistograms(histograms);
EXPECT_FALSE(fetcher->primary_icon().drawsNothing());
EXPECT_TRUE(fetcher->shortcut_info().best_primary_icon_url.is_empty());
}
// Check that the AddToHomescreenDataFetcher::Observer methods are called if
// the service worker check times out on a page that is installable (i.e. it's
// taken too long). This should use the short_name and icon from the manifest,
// but not be WebAPK-compatible. Only relevant when checking WebAPK
// compatibility.
TEST_F(AddToHomescreenDataFetcherTest, ServiceWorkerCheckTimesOutPwa) {
SetManifest(BuildDefaultManifest());
SetShouldInstallableTimeOut(true);
// Check where InstallableManager finishes working after the timeout and
// determines PWA-ness.
base::HistogramTester histograms;
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kDefaultManifestShortName,
blink::mojom::DisplayMode::kStandalone, false);
CheckHistograms(histograms);
EXPECT_FALSE(fetcher->primary_icon().drawsNothing());
EXPECT_EQ(fetcher->shortcut_info().best_primary_icon_url,
GURL(kDefaultIconUrl));
}
TEST_F(AddToHomescreenDataFetcherTest, ServiceWorkerCheckTimesOutNonPwa) {
SetManifest(BuildDefaultManifest());
SetShouldInstallableTimeOut(true);
SetInstallable(false);
// Check where InstallableManager finishes working after the timeout and
// determines non-PWA-ness.
base::HistogramTester histograms;
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kDefaultManifestShortName,
blink::mojom::DisplayMode::kStandalone, false);
CheckHistograms(histograms);
EXPECT_FALSE(fetcher->primary_icon().drawsNothing());
EXPECT_EQ(fetcher->shortcut_info().best_primary_icon_url,
GURL(kDefaultIconUrl));
}
TEST_F(AddToHomescreenDataFetcherTest, ServiceWorkerCheckTimesOutUnknown) {
SetManifest(BuildDefaultManifest());
SetShouldInstallableTimeOut(true);
SetInstallable(false);
// Check where InstallableManager doesn't finish working after the timeout.
// This is akin to waiting for a service worker forever.
base::HistogramTester histograms;
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kDefaultManifestShortName,
blink::mojom::DisplayMode::kStandalone, false);
// Navigate to ensure the histograms are written.
NavigateAndCommit(GURL("about:blank"));
CheckHistograms(histograms);
EXPECT_FALSE(fetcher->primary_icon().drawsNothing());
EXPECT_EQ(fetcher->shortcut_info().best_primary_icon_url,
GURL(kDefaultIconUrl));
}
TEST_F(AddToHomescreenDataFetcherTest, InstallableManifest) {
// Test a site that has an offline-capable service worker.
blink::Manifest manifest(BuildDefaultManifest());
SetManifest(manifest);
base::HistogramTester histograms;
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kDefaultManifestShortName,
kDefaultManifestName, blink::mojom::DisplayMode::kStandalone,
true);
// There should always be a primary icon.
EXPECT_FALSE(fetcher->primary_icon().drawsNothing());
EXPECT_EQ(fetcher->shortcut_info().best_primary_icon_url,
GURL(kDefaultIconUrl));
// Check that splash icon url has been selected.
EXPECT_EQ(fetcher->shortcut_info().splash_image_url, GURL(kDefaultIconUrl));
CheckHistograms(histograms);
}
TEST_F(AddToHomescreenDataFetcherTest, ManifestNameClobbersWebApplicationName) {
// Test that when the manifest provides Manifest::name but not
// Manifest::short_name that Manifest::name is used as the title.
{
// Check the case where we have no icons.
blink::Manifest manifest = BuildDefaultManifest();
manifest.icons.clear();
manifest.short_name = base::nullopt;
SetManifest(manifest);
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kDefaultManifestName,
blink::mojom::DisplayMode::kStandalone, false);
EXPECT_TRUE(fetcher->shortcut_info().best_primary_icon_url.is_empty());
EXPECT_TRUE(base::EqualsASCII(fetcher->shortcut_info().short_name,
kDefaultManifestName));
}
blink::Manifest manifest(BuildDefaultManifest());
manifest.short_name = base::nullopt;
SetManifest(manifest);
{
// Check a site with no offline-capable service worker.
SetInstallable(false);
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kDefaultManifestName,
blink::mojom::DisplayMode::kStandalone, false);
EXPECT_FALSE(fetcher->primary_icon().drawsNothing());
EXPECT_EQ(fetcher->shortcut_info().best_primary_icon_url,
GURL(kDefaultIconUrl));
EXPECT_TRUE(base::EqualsASCII(fetcher->shortcut_info().short_name,
kDefaultManifestName));
}
{
// Check a site where we time out waiting for the service worker.
SetShouldInstallableTimeOut(true);
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kDefaultManifestName,
blink::mojom::DisplayMode::kStandalone, false);
EXPECT_FALSE(fetcher->primary_icon().drawsNothing());
EXPECT_EQ(fetcher->shortcut_info().best_primary_icon_url,
GURL(kDefaultIconUrl));
EXPECT_TRUE(base::EqualsASCII(fetcher->shortcut_info().short_name,
kDefaultManifestName));
}
{
// Check a site with an offline-capable service worker.
SetInstallable(true);
SetShouldInstallableTimeOut(false);
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kDefaultManifestName,
blink::mojom::DisplayMode::kStandalone, true);
EXPECT_FALSE(fetcher->primary_icon().drawsNothing());
EXPECT_EQ(fetcher->shortcut_info().best_primary_icon_url,
GURL(kDefaultIconUrl));
EXPECT_TRUE(base::EqualsASCII(fetcher->shortcut_info().short_name,
kDefaultManifestName));
}
}
TEST_F(AddToHomescreenDataFetcherTest, ManifestNoNameNoShortName) {
// Test that when the manifest does not provide either Manifest::short_name
// nor Manifest::name that:
// - The page is not WebAPK compatible.
// - WebApplicationInfo::title is used as the "name".
// - We still use the icons from the manifest.
blink::Manifest manifest(BuildDefaultManifest());
manifest.name = base::nullopt;
manifest.short_name = base::nullopt;
// Check the case where we don't time out waiting for the service worker.
SetManifest(manifest);
ObserverWaiter waiter;
std::unique_ptr<AddToHomescreenDataFetcher> fetcher = BuildFetcher(&waiter);
RunFetcher(fetcher.get(), waiter, kWebApplicationInfoTitle,
blink::mojom::DisplayMode::kStandalone, false);
EXPECT_TRUE(base::EqualsASCII(fetcher->shortcut_info().name,
kWebApplicationInfoTitle));
EXPECT_TRUE(base::EqualsASCII(fetcher->shortcut_info().short_name,
kWebApplicationInfoTitle));
EXPECT_FALSE(fetcher->primary_icon().drawsNothing());
EXPECT_EQ(fetcher->shortcut_info().best_primary_icon_url,
GURL(kDefaultIconUrl));
}