Commit 7be9e7be authored by Glen Robertson's avatar Glen Robertson Committed by Chromium LUCI CQ

webapp:Update manifest validation to always require a Purpose:ANY icon.

Apps should always declare at least one Purpose::ANY icon (or no purpose
declared) to be handled properly on all platforms. Even when a maskable
icon is preferred as the primary icon (eg. on Android) an 'any' purpose
icon is still needed for other contexts.

Companion CL to update DevTools error messages: crrev.com/c/2557277

Bug: 1151835
Change-Id: I1995b1677d1392a5e852e544f34435826cce4e7c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2599733Reviewed-by: default avatarDominick Ng <dominickn@chromium.org>
Commit-Queue: Glen Robertson <glenrob@chromium.org>
Cr-Commit-Position: refs/heads/master@{#840070}
parent 256556ee
...@@ -139,8 +139,7 @@ class TestInstallableManager : public InstallableManager { ...@@ -139,8 +139,7 @@ class TestInstallableManager : public InstallableManager {
is_installable = false; is_installable = false;
} else if (params.valid_manifest && params.has_worker) { } else if (params.valid_manifest && params.has_worker) {
if (!IsManifestValidForWebApp(manifest_, if (!IsManifestValidForWebApp(manifest_,
true /* check_webapp_manifest_display */, true /* check_webapp_manifest_display */)) {
false /* prefer_maskable_icon */)) {
code = valid_manifest_->errors.at(0); code = valid_manifest_->errors.at(0);
is_installable = false; is_installable = false;
} else if (!is_installable_) { } else if (!is_installable_) {
......
...@@ -138,44 +138,23 @@ bool IsIconTypeSupported(const blink::Manifest::ImageResource& icon) { ...@@ -138,44 +138,23 @@ bool IsIconTypeSupported(const blink::Manifest::ImageResource& icon) {
return false; return false;
} }
// Returns true if |manifest| specifies an SVG or PNG icon that either // Returns whether |manifest| specifies an SVG or PNG icon that has
// 1. has IconPurpose::ANY, with height and width >= kMinimumPrimaryIconSizeInPx // IconPurpose::ANY, with size >= kMinimumPrimaryIconSizeInPx (or size "any").
// (or size "any") bool DoesManifestContainRequiredIcon(const blink::Manifest& manifest) {
// 2. if maskable icon is preferred, has IconPurpose::MASKABLE with height and
// width >= kMinimumPrimaryAdaptiveLauncherIconSizeInPx (or size "any")
bool DoesManifestContainRequiredIcon(const blink::Manifest& manifest,
bool prefer_maskable_icon) {
for (const auto& icon : manifest.icons) { for (const auto& icon : manifest.icons) {
if (!IsIconTypeSupported(icon)) if (!IsIconTypeSupported(icon))
continue; continue;
if (!(base::Contains(icon.purpose, if (!base::Contains(icon.purpose, IconPurpose::ANY))
blink::mojom::ManifestImageResource_Purpose::ANY) ||
(prefer_maskable_icon &&
base::Contains(
icon.purpose,
blink::mojom::ManifestImageResource_Purpose::MASKABLE)))) {
continue; continue;
}
for (const auto& size : icon.sizes) { for (const auto& size : icon.sizes) {
if (size.IsEmpty()) // "any" if (size.IsEmpty()) // "any"
return true; return true;
if (base::Contains(icon.purpose, if (size.width() >= kMinimumPrimaryIconSizeInPx &&
blink::mojom::ManifestImageResource_Purpose::ANY) &&
size.width() >= kMinimumPrimaryIconSizeInPx &&
size.height() >= kMinimumPrimaryIconSizeInPx) { size.height() >= kMinimumPrimaryIconSizeInPx) {
return true; return true;
} }
if (prefer_maskable_icon &&
base::Contains(
icon.purpose,
blink::mojom::ManifestImageResource_Purpose::MASKABLE) &&
size.height() >= kMinimumPrimaryAdaptiveLauncherIconSizeInPx &&
size.width() >= kMinimumPrimaryAdaptiveLauncherIconSizeInPx) {
return true;
}
} }
} }
...@@ -581,8 +560,7 @@ void InstallableManager::WorkOnTask() { ...@@ -581,8 +560,7 @@ void InstallableManager::WorkOnTask() {
GetMinimumPrimaryIconSizeInPx(), IconPurpose::ANY, GetMinimumPrimaryIconSizeInPx(), IconPurpose::ANY,
IconUsage::kPrimary); IconUsage::kPrimary);
} else if (params.valid_manifest && !valid_manifest_->fetched) { } else if (params.valid_manifest && !valid_manifest_->fetched) {
CheckManifestValid(params.check_webapp_manifest_display, CheckManifestValid(params.check_webapp_manifest_display);
params.prefer_maskable_icon);
} else if (params.fetch_screenshots && !IsScreenshotsFetchComplete()) { } else if (params.fetch_screenshots && !IsScreenshotsFetchComplete()) {
CheckAndFetchScreenshots(); CheckAndFetchScreenshots();
} else if (params.has_worker && !worker_->fetched) { } else if (params.has_worker && !worker_->fetched) {
...@@ -640,21 +618,20 @@ void InstallableManager::OnDidGetManifest(const GURL& manifest_url, ...@@ -640,21 +618,20 @@ void InstallableManager::OnDidGetManifest(const GURL& manifest_url,
WorkOnTask(); WorkOnTask();
} }
void InstallableManager::CheckManifestValid(bool check_webapp_manifest_display, void InstallableManager::CheckManifestValid(
bool prefer_maskable_icon) { bool check_webapp_manifest_display) {
DCHECK(!valid_manifest_->fetched); DCHECK(!valid_manifest_->fetched);
DCHECK(!manifest().IsEmpty()); DCHECK(!manifest().IsEmpty());
valid_manifest_->is_valid = IsManifestValidForWebApp( valid_manifest_->is_valid =
manifest(), check_webapp_manifest_display, prefer_maskable_icon); IsManifestValidForWebApp(manifest(), check_webapp_manifest_display);
valid_manifest_->fetched = true; valid_manifest_->fetched = true;
WorkOnTask(); WorkOnTask();
} }
bool InstallableManager::IsManifestValidForWebApp( bool InstallableManager::IsManifestValidForWebApp(
const blink::Manifest& manifest, const blink::Manifest& manifest,
bool check_webapp_manifest_display, bool check_webapp_manifest_display) {
bool prefer_maskable_icon) {
bool is_valid = true; bool is_valid = true;
if (manifest.IsEmpty()) { if (manifest.IsEmpty()) {
valid_manifest_->errors.push_back(MANIFEST_EMPTY); valid_manifest_->errors.push_back(MANIFEST_EMPTY);
...@@ -694,7 +671,7 @@ bool InstallableManager::IsManifestValidForWebApp( ...@@ -694,7 +671,7 @@ bool InstallableManager::IsManifestValidForWebApp(
} }
} }
if (!DoesManifestContainRequiredIcon(manifest, prefer_maskable_icon)) { if (!DoesManifestContainRequiredIcon(manifest)) {
valid_manifest_->errors.push_back(MANIFEST_MISSING_SUITABLE_ICON); valid_manifest_->errors.push_back(MANIFEST_MISSING_SUITABLE_ICON);
is_valid = false; is_valid = false;
} }
......
...@@ -213,11 +213,9 @@ class InstallableManager ...@@ -213,11 +213,9 @@ class InstallableManager
void OnDidGetManifest(const GURL& manifest_url, void OnDidGetManifest(const GURL& manifest_url,
const blink::Manifest& manifest); const blink::Manifest& manifest);
void CheckManifestValid(bool check_webapp_manifest_display, void CheckManifestValid(bool check_webapp_manifest_display);
bool prefer_maskable_icon);
bool IsManifestValidForWebApp(const blink::Manifest& manifest, bool IsManifestValidForWebApp(const blink::Manifest& manifest,
bool check_webapp_manifest_display, bool check_webapp_manifest_display);
bool prefer_maskable_icon);
void CheckServiceWorker(); void CheckServiceWorker();
void OnDidCheckHasServiceWorker( void OnDidCheckHasServiceWorker(
base::TimeTicks check_service_worker_start_time, base::TimeTicks check_service_worker_start_time,
......
...@@ -43,12 +43,11 @@ class InstallableManagerUnitTest : public testing::Test { ...@@ -43,12 +43,11 @@ class InstallableManagerUnitTest : public testing::Test {
} }
bool IsManifestValid(const blink::Manifest& manifest, bool IsManifestValid(const blink::Manifest& manifest,
bool check_webapp_manifest_display = true, bool check_webapp_manifest_display = true) {
bool prefer_maskable_icon = false) {
// Explicitly reset the error code before running the method. // Explicitly reset the error code before running the method.
manager_->set_valid_manifest_error(NO_ERROR_DETECTED); manager_->set_valid_manifest_error(NO_ERROR_DETECTED);
return manager_->IsManifestValidForWebApp( return manager_->IsManifestValidForWebApp(manifest,
manifest, check_webapp_manifest_display, prefer_maskable_icon); check_webapp_manifest_display);
} }
InstallableStatusCode GetErrorCode() { InstallableStatusCode GetErrorCode() {
...@@ -218,32 +217,6 @@ TEST_F(InstallableManagerUnitTest, ManifestRequiresPurposeAny) { ...@@ -218,32 +217,6 @@ TEST_F(InstallableManagerUnitTest, ManifestRequiresPurposeAny) {
EXPECT_EQ(NO_ERROR_DETECTED, GetErrorCode()); EXPECT_EQ(NO_ERROR_DETECTED, GetErrorCode());
} }
TEST_F(InstallableManagerUnitTest,
ManifestRequiresPurposeAnyOrMaskableWhenPreferringMaskable) {
blink::Manifest manifest = GetValidManifest();
EXPECT_TRUE(IsManifestValid(manifest, true, true));
EXPECT_EQ(NO_ERROR_DETECTED, GetErrorCode());
manifest.icons[0].purpose[0] = IconPurpose::MASKABLE;
EXPECT_TRUE(IsManifestValid(manifest, true, true));
EXPECT_EQ(NO_ERROR_DETECTED, GetErrorCode());
// The icon MUST have IconPurpose::ANY or IconPurpose::Maskable.
manifest.icons[0].purpose[0] = IconPurpose::MONOCHROME;
EXPECT_FALSE(IsManifestValid(manifest, true, true));
EXPECT_EQ(MANIFEST_MISSING_SUITABLE_ICON, GetErrorCode());
// If one of the icon purposes match the requirement, it should be accepted.
manifest.icons[0].purpose.push_back(IconPurpose::ANY);
EXPECT_TRUE(IsManifestValid(manifest, true, true));
EXPECT_EQ(NO_ERROR_DETECTED, GetErrorCode());
manifest.icons[0].purpose[1] = IconPurpose::MASKABLE;
EXPECT_TRUE(IsManifestValid(manifest, true, true));
EXPECT_EQ(NO_ERROR_DETECTED, GetErrorCode());
}
TEST_F(InstallableManagerUnitTest, ManifestRequiresMinimalSize) { TEST_F(InstallableManagerUnitTest, ManifestRequiresMinimalSize) {
blink::Manifest manifest = GetValidManifest(); blink::Manifest manifest = GetValidManifest();
...@@ -275,32 +248,6 @@ TEST_F(InstallableManagerUnitTest, ManifestRequiresMinimalSize) { ...@@ -275,32 +248,6 @@ TEST_F(InstallableManagerUnitTest, ManifestRequiresMinimalSize) {
manifest.icons[0].sizes[1] = gfx::Size(0, 0); manifest.icons[0].sizes[1] = gfx::Size(0, 0);
EXPECT_TRUE(IsManifestValid(manifest)); EXPECT_TRUE(IsManifestValid(manifest));
EXPECT_EQ(NO_ERROR_DETECTED, GetErrorCode()); EXPECT_EQ(NO_ERROR_DETECTED, GetErrorCode());
// When preferring maskable icons, both non-maskable and maskable
// icons are smaller than required.
manifest.icons[0].sizes.clear();
manifest.icons[0].sizes.push_back(gfx::Size(1, 1));
blink::Manifest::ImageResource maskable_icon;
maskable_icon.type = base::ASCIIToUTF16("image/png");
maskable_icon.sizes.push_back(gfx::Size(82, 82));
maskable_icon.purpose.push_back(IconPurpose::MASKABLE);
manifest.icons.push_back(maskable_icon);
EXPECT_FALSE(IsManifestValid(manifest, true, true));
EXPECT_EQ(MANIFEST_MISSING_SUITABLE_ICON, GetErrorCode());
// When preferring maskable icons, the maskable icon is smaller than required.
manifest.icons[0].sizes[0] = gfx::Size(144, 144);
manifest.icons[1].sizes[0] = gfx::Size(82, 82);
EXPECT_TRUE(IsManifestValid(manifest, true, true));
EXPECT_EQ(NO_ERROR_DETECTED, GetErrorCode());
// When preferring maskable icons, the maskable icon satisfies the size
// requirement though the non maskable one doesn't
manifest.icons[0].sizes[0] = gfx::Size(1, 1);
manifest.icons[1].sizes[0] = gfx::Size(83, 83);
EXPECT_TRUE(IsManifestValid(manifest, true, true));
EXPECT_EQ(NO_ERROR_DETECTED, GetErrorCode());
} }
TEST_F(InstallableManagerUnitTest, ManifestDisplayModes) { TEST_F(InstallableManagerUnitTest, ManifestDisplayModes) {
......
...@@ -29,6 +29,7 @@ struct InstallableParams { ...@@ -29,6 +29,7 @@ struct InstallableParams {
bool valid_primary_icon = false; bool valid_primary_icon = false;
// Whether to prefer an icon with purpose 'maskable' for the primary icon. // Whether to prefer an icon with purpose 'maskable' for the primary icon.
// An icon with purpose 'any' is still required for a valid manifest.
bool prefer_maskable_icon = false; bool prefer_maskable_icon = false;
// Check whether there is a fetchable, non-empty icon in the manifest // Check whether there is a fetchable, non-empty icon in the manifest
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment