Commit 2adab305 authored by Anatoliy Potapchuk's avatar Anatoliy Potapchuk Committed by Commit Bot

[Kiosk] Fetch the icon provided by the policy for Web App kiosk

Currently, before the first launch of web kiosk app, the icon is not
loaded and remains default until user installs the app for the first
time. This can be somewhat inconvenient.

In this cl we will use url provided by policy to download the icon.

Bug: 1066496
Change-Id: I2b7a105723b4ff62748e95e211f1c9e67a11c077
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2257896Reviewed-by: default avatarAnqing Zhao <anqing@chromium.org>
Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Commit-Queue: Anatoliy Potapchuk <apotapchuk@chromium.org>
Cr-Commit-Position: refs/heads/master@{#782039}
parent f076d610
......@@ -73,6 +73,14 @@ void KioskAppDataBase::SaveToDictionary(DictionaryPrefUpdate& dict_update) {
dict_update->SetString(icon_path_key, icon_path_.value());
}
void KioskAppDataBase::SaveIconToDictionary(DictionaryPrefUpdate& dict_update) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
const std::string app_key = std::string(kKeyApps) + '.' + app_id_;
const std::string icon_path_key = app_key + '.' + kKeyIcon;
dict_update->SetString(icon_path_key, icon_path_.value());
}
bool KioskAppDataBase::LoadFromDictionary(const base::DictionaryValue& dict,
bool lazy_icon_load) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
......@@ -82,12 +90,15 @@ bool KioskAppDataBase::LoadFromDictionary(const base::DictionaryValue& dict,
const std::string icon_path_key = app_key + '.' + kKeyIcon;
std::string icon_path_string;
if (!dict.GetString(name_key, &name_) ||
!dict.GetString(icon_path_key, &icon_path_string)) {
// If there is no title stored, do not stop, sometimes only icon is cached.
dict.GetString(name_key, &name_);
if (!dict.GetString(icon_path_key, &icon_path_string)) {
return false;
}
icon_path_ = base::FilePath(icon_path_string);
if (!lazy_icon_load) {
DecodeIcon();
}
......
......@@ -47,6 +47,9 @@ class KioskAppDataBase : public KioskAppIconLoader::Delegate {
// Helper to save name and icon to provided dictionary.
void SaveToDictionary(DictionaryPrefUpdate& dict_update);
// Helper to save icon to provided dictionary.
void SaveIconToDictionary(DictionaryPrefUpdate& dict_update);
// Helper to load name and icon from provided dictionary.
// if |lazy_icon_load| is set to true, the icon will not be updated, only
// icon_path_.
......
......@@ -16,13 +16,13 @@ namespace chromeos {
class KioskAppDataDelegate {
public:
// Invoked to get the root directory for storing cached icon files.
virtual void GetKioskAppIconCacheDir(base::FilePath* cache_dir) const = 0;
virtual void GetKioskAppIconCacheDir(base::FilePath* cache_dir) = 0;
// Invoked when kiosk app data or status has changed.
virtual void OnKioskAppDataChanged(const std::string& app_id) const = 0;
virtual void OnKioskAppDataChanged(const std::string& app_id) = 0;
// Invoked when failed to load web store data of an app.
virtual void OnKioskAppDataLoadFailure(const std::string& app_id) const = 0;
virtual void OnKioskAppDataLoadFailure(const std::string& app_id) = 0;
// Invoked when the data which is stored in ExternalCache for current app is
// damaged.
......
......@@ -51,22 +51,19 @@ KioskAppManagerBase::App::App(const App&) = default;
KioskAppManagerBase::App::~App() = default;
void KioskAppManagerBase::GetKioskAppIconCacheDir(
base::FilePath* cache_dir) const {
void KioskAppManagerBase::GetKioskAppIconCacheDir(base::FilePath* cache_dir) {
base::FilePath user_data_dir;
bool has_dir = base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
DCHECK(has_dir);
*cache_dir = user_data_dir.AppendASCII(kIconCacheDir);
}
void KioskAppManagerBase::OnKioskAppDataChanged(
const std::string& app_id) const {
void KioskAppManagerBase::OnKioskAppDataChanged(const std::string& app_id) {
for (auto& observer : observers_)
observer.OnKioskAppDataChanged(app_id);
}
void KioskAppManagerBase::OnKioskAppDataLoadFailure(
const std::string& app_id) const {
void KioskAppManagerBase::OnKioskAppDataLoadFailure(const std::string& app_id) {
for (auto& observer : observers_)
observer.OnKioskAppDataLoadFailure(app_id);
}
......
......@@ -12,8 +12,10 @@
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/path_service.h"
#include "chrome/browser/chromeos/app_mode/kiosk_app_data_delegate.h"
#include "chrome/browser/chromeos/settings/cros_settings.h"
#include "chrome/common/chrome_paths.h"
#include "components/account_id/account_id.h"
#include "ui/gfx/image/image_skia.h"
#include "url/gurl.h"
......@@ -59,9 +61,9 @@ class KioskAppManagerBase : public KioskAppDataDelegate {
void RemoveObserver(KioskAppManagerObserver* observer);
// KioskAppDataDelegate overrides:
void GetKioskAppIconCacheDir(base::FilePath* cache_dir) const override;
void OnKioskAppDataChanged(const std::string& app_id) const override;
void OnKioskAppDataLoadFailure(const std::string& app_id) const override;
void GetKioskAppIconCacheDir(base::FilePath* cache_dir) override;
void OnKioskAppDataChanged(const std::string& app_id) override;
void OnKioskAppDataLoadFailure(const std::string& app_id) override;
void OnExternalCacheDamaged(const std::string& app_id) override;
// Gets whether the bailout shortcut is disabled.
......
......@@ -5,21 +5,144 @@
#include "chrome/browser/chromeos/app_mode/web_app/web_kiosk_app_data.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/app_mode/kiosk_app_data_delegate.h"
#include "chrome/browser/chromeos/app_mode/web_app/web_kiosk_app_manager.h"
#include "chrome/browser/image_decoder/image_decoder.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/common/web_application_info.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "skia/ext/image_operations.h"
namespace chromeos {
namespace {
constexpr int kIconSize = 128; // size of the icon in px.
// Maximum image size is 256x256..
constexpr int kMaxIconFileSize = (2 * kIconSize) * (2 * kIconSize) * 4 + 1000;
const char kKeyLaunchUrl[] = "launch_url";
// Resizes image into other size on blocking I/O thread.
SkBitmap ResizeImageBlocking(const SkBitmap& image, int target_size) {
return skia::ImageOperations::Resize(
image, skia::ImageOperations::RESIZE_BEST, target_size, target_size);
}
} // namespace
class WebKioskAppData::IconFetcher : public ImageDecoder::ImageRequest {
public:
IconFetcher(const base::WeakPtr<WebKioskAppData>& client,
const GURL& icon_url)
: client_(client), icon_url_(icon_url) {}
void Start() {
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("kiosk_app_icon", R"(
semantics {
sender: "Kiosk App Icon Downloader"
description:
"The actual meta-data of the kiosk apps that is used to "
"display the application info can only be obtained upon "
"the installation of the app itself. Before this happens, "
"we are using default placeholder icon for the app. To "
"overcome this issue, the URL with the icon file is being "
"sent from the device management server. Chromium will "
"download the image located at this url."
trigger:
"User clicks on the menu button with the list of kiosk apps"
data: "None"
destination: WEBSITE
}
policy {
cookies_allowed: NO
setting: "This feature cannot be disabled in settings."
policy_exception_justification:
"No content is being uploaded or saved; this request merely "
"downloads a publicly available PNG file."
})");
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = icon_url_;
simple_loader_ = network::SimpleURLLoader::Create(
std::move(resource_request), traffic_annotation);
simple_loader_->SetRetryOptions(
/* max_retries=*/3,
network::SimpleURLLoader::RETRY_ON_5XX |
network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
SystemNetworkContextManager* system_network_context_manager =
g_browser_process->system_network_context_manager();
network::mojom::URLLoaderFactory* loader_factory =
system_network_context_manager->GetURLLoaderFactory();
simple_loader_->DownloadToString(
loader_factory,
base::BindOnce(
[](base::WeakPtr<WebKioskAppData> client,
std::unique_ptr<std::string> response_body) {
if (!client)
return;
client->icon_fetcher_->OnSimpleLoaderComplete(
std::move(response_body));
},
client_),
kMaxIconFileSize);
}
void OnSimpleLoaderComplete(std::unique_ptr<std::string> response_body) {
if (!response_body) {
LOG(ERROR) << "Could not download icon url for kiosk app.";
return;
}
// Call start to begin decoding. The ImageDecoder will call OnImageDecoded
// with the data when it is done.
ImageDecoder::Start(this, *response_body);
}
private:
// ImageDecoder::ImageRequest:
void OnImageDecoded(const SkBitmap& decoded_image) override {
if (!client_)
return;
// Icons have to be square shaped.
if (decoded_image.width() != decoded_image.height()) {
LOG(ERROR) << "Received kiosk icon of invalid shape.";
return;
}
int size = decoded_image.width();
if (size == kIconSize) {
client_->OnDidDownloadIcon(decoded_image);
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(ResizeImageBlocking, decoded_image, kIconSize),
base::BindOnce(&WebKioskAppData::OnDidDownloadIcon, client_));
}
void OnDecodeImageFailed() override {
// Do nothing.
LOG(ERROR) << "Could not download icon url for kiosk app.";
}
base::WeakPtr<WebKioskAppData> client_;
const GURL icon_url_;
std::unique_ptr<network::SimpleURLLoader> simple_loader_;
};
WebKioskAppData::WebKioskAppData(KioskAppDataDelegate* delegate,
const std::string& app_id,
const AccountId& account_id,
......@@ -66,7 +189,17 @@ void WebKioskAppData::LoadIcon() {
DecodeIcon();
return;
}
NOTIMPLEMENTED();
if (!icon_url_.is_valid())
return;
DCHECK(!icon_fetcher_);
status_ = STATUS_LOADING;
icon_fetcher_ = std::make_unique<WebKioskAppData::IconFetcher>(
weak_ptr_factory_.GetWeakPtr(), icon_url_);
icon_fetcher_->Start();
}
void WebKioskAppData::UpdateFromWebAppInfo(
......@@ -119,6 +252,27 @@ bool WebKioskAppData::LoadLaunchUrlFromDictionary(const base::Value& dict) {
return true;
}
void WebKioskAppData::OnDidDownloadIcon(const SkBitmap& icon) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::unique_ptr<IconFetcher> fetcher = std::move(icon_fetcher_);
if (status_ == STATUS_INSTALLED)
return;
base::FilePath cache_dir;
if (delegate_)
delegate_->GetKioskAppIconCacheDir(&cache_dir);
SaveIcon(icon, cache_dir);
PrefService* local_state = g_browser_process->local_state();
DictionaryPrefUpdate dict_update(local_state, dictionary_name());
SaveIconToDictionary(dict_update);
SetStatus(STATUS_LOADED);
}
void WebKioskAppData::OnIconLoadSuccess(const gfx::ImageSkia& icon) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
kiosk_app_icon_loader_.reset();
......
......@@ -41,7 +41,7 @@ class WebKioskAppData : public KioskAppDataBase {
// Loads the locally cached data. Returns true on success.
bool LoadFromCache();
// Updates |icon_| from either |KioskAppDataBase::icon_url_| or |icon_url_|.
// Updates |icon_| from either |KioskAppDataBase::icon_path_| or |icon_url_|.
void LoadIcon();
// KioskAppDataBase overrides:
......@@ -57,15 +57,21 @@ class WebKioskAppData : public KioskAppDataBase {
const GURL& launch_url() const { return launch_url_; }
private:
class IconFetcher;
void OnDidDownloadIcon(const SkBitmap& icon);
bool LoadLaunchUrlFromDictionary(const base::Value& dict);
const KioskAppDataDelegate* delegate_; // not owned.
KioskAppDataDelegate* delegate_; // not owned.
Status status_;
const GURL install_url_; // installation url.
GURL launch_url_; // app launch url.
// TODO(crbug.com/1066496): Start fetching the icon when kiosk menu is opened.
const GURL icon_url_; // Url of the icon in case nothing is cached.
// Used to download icon from |icon_url_|.
std::unique_ptr<IconFetcher> icon_fetcher_;
base::WeakPtrFactory<WebKioskAppData> weak_ptr_factory_{this};
DISALLOW_COPY_AND_ASSIGN(WebKioskAppData);
};
......
// Copyright 2020 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/chromeos/app_mode/web_app/web_kiosk_app_data.h"
#include "base/path_service.h"
#include "chrome/browser/chromeos/app_mode/kiosk_app_data_delegate.h"
#include "chrome/browser/chromeos/app_mode/web_app/web_kiosk_app_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/scoped_testing_local_state.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_test.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace chromeos {
namespace {
const char kAppId[] = "123";
const char kAppUrl[] = "https://example.com/";
const char kAppKey[] = "apps";
const char kAppTitle[] = "Title";
const char kAppTitle2[] = "Title2";
const char kTitleKey[] = "name";
const char kIconKey[] = "icon";
const char kLaunchUrlKey[] = "launch_url";
const char kIconPath[] = "chrome/test/data/load_image/image.png";
const char kIconUrl[] = "/load_image/image.png";
const char kLaunchUrl[] = "https://example.com/launch";
base::FilePath GetFullPathToImage() {
base::FilePath test_data_dir;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &test_data_dir));
return test_data_dir.Append(kIconPath);
}
} // namespace
class WebKioskAppDataTest : public InProcessBrowserTest,
public KioskAppDataDelegate {
public:
void WaitForAppDataChange(int count) {
if (change_count_ >= count)
return;
waited_count_ = count;
waiter_ = std::make_unique<base::RunLoop>();
waiter_->Run();
}
void SetCached(bool installed) {
const std::string app_key = std::string(kAppKey) + '.' + kAppId;
auto app_dict = std::make_unique<base::DictionaryValue>();
app_dict->SetString(app_key + '.' + std::string(kTitleKey), kAppTitle);
app_dict->SetString(app_key + '.' + std::string(kIconKey),
GetFullPathToImage().value());
if (installed)
app_dict->SetString(app_key + '.' + std::string(kLaunchUrlKey),
kLaunchUrl);
g_browser_process->local_state()->Set(
WebKioskAppManager::kWebKioskDictionaryName, *app_dict);
}
private:
// KioskAppDataDelegate:
void GetKioskAppIconCacheDir(base::FilePath* cache_dir) override {
base::FilePath user_data_dir;
bool has_dir =
base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
DCHECK(has_dir);
*cache_dir = user_data_dir;
}
void OnKioskAppDataChanged(const std::string& app_id) override {
change_count_++;
if (change_count_ >= waited_count_ && waiter_)
waiter_->Quit();
}
void OnKioskAppDataLoadFailure(const std::string& app_id) override {}
void OnExternalCacheDamaged(const std::string& app_id) override {}
// std::unique_ptr<ScopedTestingLocalState> local_state_;
std::unique_ptr<base::RunLoop> waiter_;
int change_count_ = 0;
int waited_count_ = 0;
};
IN_PROC_BROWSER_TEST_F(WebKioskAppDataTest, NoIconCached) {
WebKioskAppData app_data(this, kAppId, EmptyAccountId(), GURL(kAppUrl),
std::string(), /*icon_url*/ GURL());
app_data.LoadFromCache();
// The app will stay in the INIT state if there is nothing to be loaded from
// cache.
EXPECT_EQ(app_data.status(), WebKioskAppData::STATUS_INIT);
EXPECT_EQ(app_data.name(), kAppUrl);
EXPECT_TRUE(app_data.icon().isNull());
}
IN_PROC_BROWSER_TEST_F(WebKioskAppDataTest, LoadCachedIcon) {
SetCached(/*installed = */ false);
WebKioskAppData app_data(this, kAppId, EmptyAccountId(), GURL(kAppUrl),
std::string(), /*icon_url*/ GURL());
app_data.LoadFromCache();
app_data.LoadIcon();
WaitForAppDataChange(2);
EXPECT_EQ(app_data.status(), WebKioskAppData::STATUS_LOADED);
EXPECT_EQ(app_data.name(), kAppTitle);
EXPECT_FALSE(app_data.icon().isNull());
}
IN_PROC_BROWSER_TEST_F(WebKioskAppDataTest, PRE_DownloadedIconPersists) {
// Start test server.
net::EmbeddedTestServer test_server;
test_server.AddDefaultHandlers(GetChromeTestDataDir());
ASSERT_TRUE(test_server.Start());
// SetCachedNameAndIcon();
WebKioskAppData app_data(this, kAppId, EmptyAccountId(), GURL(kAppUrl),
kAppTitle,
/*icon_url*/ test_server.GetURL(kIconUrl));
app_data.LoadFromCache();
app_data.LoadIcon();
WaitForAppDataChange(1);
EXPECT_EQ(app_data.status(), WebKioskAppData::STATUS_LOADED);
EXPECT_EQ(app_data.name(), kAppTitle);
}
IN_PROC_BROWSER_TEST_F(WebKioskAppDataTest, DownloadedIconPersists) {
// No test server is launched intentionaly to verify that we are using the
// cached icon.
WebKioskAppData app_data(this, kAppId, EmptyAccountId(), GURL(kAppUrl),
kAppTitle2, /*icon_url=*/GURL());
app_data.LoadFromCache();
app_data.LoadIcon();
WaitForAppDataChange(2);
EXPECT_EQ(app_data.status(), WebKioskAppData::STATUS_LOADED);
// The title should not persist.
EXPECT_EQ(app_data.name(), kAppTitle2);
}
IN_PROC_BROWSER_TEST_F(WebKioskAppDataTest, AlreadyInstalled) {
SetCached(/*installed = */ true);
WebKioskAppData app_data(this, kAppId, EmptyAccountId(), GURL(kAppUrl),
kAppTitle2, /*icon_url=*/GURL());
app_data.LoadFromCache();
app_data.LoadIcon();
WaitForAppDataChange(2);
EXPECT_EQ(app_data.status(), WebKioskAppData::STATUS_INSTALLED);
EXPECT_EQ(app_data.name(), kAppTitle);
}
} // namespace chromeos
......@@ -2100,6 +2100,7 @@ if (!is_android) {
"../browser/chromeos/app_mode/kiosk_app_manager_browsertest.cc",
"../browser/chromeos/app_mode/kiosk_app_update_service_browsertest.cc",
"../browser/chromeos/app_mode/kiosk_crash_restore_browsertest.cc",
"../browser/chromeos/app_mode/web_app/web_kiosk_app_data_browsertest.cc",
"../browser/chromeos/apps/apk_web_app_installer_browsertest.cc",
"../browser/chromeos/arc/accessibility/arc_accessibility_helper_bridge_browsertest.cc",
"../browser/chromeos/arc/auth/arc_active_directory_enrollment_token_fetcher_browsertest.cc",
......
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