chromium/chrome/browser/ash/app_mode/web_app/web_kiosk_app_data.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/app_mode/web_app/web_kiosk_app_data.h"

#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "chrome/browser/ash/app_mode/kiosk_app_data_base.h"
#include "chrome/browser/ash/app_mode/kiosk_app_data_delegate.h"
#include "chrome/browser/ash/app_mode/web_app/web_kiosk_app_manager.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/image_decoder/image_decoder.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "components/account_id/account_id.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/browser_thread.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "skia/ext/image_operations.h"
#include "ui/gfx/image/image_skia.h"

namespace ash {

constexpr int kWebKioskIconSize = 128;  // size of the icon in px.

namespace {
// Maximum image size is 256x256..
constexpr int kMaxIconFileSize =
    (2 * kWebKioskIconSize) * (2 * kWebKioskIconSize) * 4 + 1000;

const char kKeyLaunchUrl[] = "launch_url";
const char kKeyLastIconUrl[] = "last_icon_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, std::move(*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 == kWebKioskIconSize) {
      client_->OnDidDownloadIcon(decoded_image);
      return;
    }

    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE,
        {base::TaskPriority::USER_VISIBLE,
         base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
        base::BindOnce(ResizeImageBlocking, decoded_image, kWebKioskIconSize),
        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,
                                 const GURL url,
                                 const std::string& title,
                                 const GURL icon_url)
    : KioskAppDataBase(WebKioskAppManager::kWebKioskDictionaryName,
                       app_id,
                       account_id),
      delegate_(delegate),
      status_(Status::kInit),
      install_url_(url),
      icon_url_(icon_url) {
  name_ = title.empty() ? install_url_.spec() : title;
}

WebKioskAppData::~WebKioskAppData() = default;

bool WebKioskAppData::LoadFromCache() {
  PrefService* local_state = g_browser_process->local_state();
  const base::Value::Dict& dict = local_state->GetDict(dictionary_name());

  if (!LoadFromDictionary(dict)) {
    return false;
  }

  if (LoadLaunchUrlFromDictionary(dict)) {
    SetStatus(Status::kInstalled);
    return true;
  }

  // If the icon was previously downloaded using a different url and the app has
  // not been installed earlier, do not use that icon.
  if (GetLastIconUrl(dict) != icon_url_) {
    return false;
  }

  // Wait while icon is loaded.
  if (status_ == Status::kInit) {
    SetStatus(Status::kLoading);
  }
  return true;
}

void WebKioskAppData::LoadIcon() {
  if (!icon_.isNull()) {
    return;
  }

  // Decode the icon if one is already cached.
  if (status_ != Status::kInit) {
    DecodeIcon(base::BindOnce(&WebKioskAppData::OnIconLoadDone,
                              weak_ptr_factory_.GetWeakPtr()));
    return;
  }

  if (!icon_url_.is_valid()) {
    return;
  }

  DCHECK(!icon_fetcher_);

  status_ = Status::kLoading;

  icon_fetcher_ = std::make_unique<WebKioskAppData::IconFetcher>(
      weak_ptr_factory_.GetWeakPtr(), icon_url_);
  icon_fetcher_->Start();
}

GURL WebKioskAppData::GetLaunchableUrl() const {
  return status() == WebKioskAppData::Status::kInstalled ? launch_url()
                                                         : install_url();
}

void WebKioskAppData::UpdateFromWebAppInfo(
    const web_app::WebAppInstallInfo& app_info) {
  UpdateAppInfo(base::UTF16ToUTF8(app_info.title), app_info.start_url(),
                app_info.icon_bitmaps);
}

void WebKioskAppData::UpdateAppInfo(const std::string& title,
                                    const GURL& start_url,
                                    const web_app::IconBitmaps& icon_bitmaps) {
  name_ = title;

  base::FilePath cache_dir;
  if (delegate_) {
    delegate_->GetKioskAppIconCacheDir(&cache_dir);
  }

  auto it = icon_bitmaps.any.find(kWebKioskIconSize);
  if (it != icon_bitmaps.any.end()) {
    const SkBitmap& bitmap = it->second;
    icon_ = gfx::ImageSkia::CreateFrom1xBitmap(bitmap);
    icon_.MakeThreadSafe();
    SaveIcon(bitmap, cache_dir);
  }

  PrefService* local_state = g_browser_process->local_state();
  ScopedDictPrefUpdate dict_update(local_state, dictionary_name());
  SaveToDictionary(dict_update);

  launch_url_ = start_url;
  dict_update->FindDict(KioskAppDataBase::kKeyApps)
      ->FindDict(app_id())
      ->Set(kKeyLaunchUrl, launch_url_.spec());

  SetStatus(Status::kInstalled);
}

void WebKioskAppData::SetOnLoadedCallbackForTesting(
    base::OnceClosure callback) {
  on_loaded_closure_for_testing_ = std::move(callback);
}

void WebKioskAppData::SetStatus(Status status, bool notify) {
  status_ = status;

  if (status_ == Status::kLoaded && on_loaded_closure_for_testing_) {
    std::move(on_loaded_closure_for_testing_).Run();
  }

  if (delegate_ && notify) {
    delegate_->OnKioskAppDataChanged(app_id());
  }
}

bool WebKioskAppData::LoadLaunchUrlFromDictionary(
    const base::Value::Dict& dict) {
  // All the previous keys should be present since this function is executed
  // after LoadFromDictionary().
  const std::string* launch_url_string =
      dict.FindDict(KioskAppDataBase::kKeyApps)
          ->FindDict(app_id())
          ->FindString(kKeyLaunchUrl);

  if (!launch_url_string) {
    return false;
  }

  launch_url_ = GURL(*launch_url_string);
  return true;
}

GURL WebKioskAppData::GetLastIconUrl(const base::Value::Dict& dict) const {
  // All the previous keys should be present since this function is executed
  // after LoadFromDictionary().
  const std::string* icon_url_string = dict.FindDict(KioskAppDataBase::kKeyApps)
                                           ->FindDict(app_id())
                                           ->FindString(kKeyLastIconUrl);

  return icon_url_string ? GURL(*icon_url_string) : GURL();
}

void WebKioskAppData::OnDidDownloadIcon(const SkBitmap& icon) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  std::unique_ptr<IconFetcher> fetcher = std::move(icon_fetcher_);

  if (status_ == Status::kInstalled) {
    return;
  }

  base::FilePath cache_dir;
  if (delegate_) {
    delegate_->GetKioskAppIconCacheDir(&cache_dir);
  }

  SaveIcon(icon, cache_dir);

  PrefService* local_state = g_browser_process->local_state();
  ScopedDictPrefUpdate dict_update(local_state, dictionary_name());
  SaveIconToDictionary(dict_update);

  dict_update->FindDict(KioskAppDataBase::kKeyApps)
      ->FindDict(app_id())
      ->Set(kKeyLastIconUrl, icon_url_.spec());

  SetStatus(Status::kLoaded);
}

void WebKioskAppData::OnIconLoadDone(std::optional<gfx::ImageSkia> icon) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  kiosk_app_icon_loader_.reset();

  if (!icon.has_value()) {
    LOG(ERROR) << "Icon Load Failure";
    SetStatus(Status::kLoaded, /*notify=*/false);
    return;
  }

  icon_ = icon.value();
  if (status_ != Status::kInstalled) {
    SetStatus(Status::kLoaded);
  } else {
    SetStatus(Status::kInstalled);  // To notify menu controller.
  }
}

}  // namespace ash