chromium/chrome/browser/apps/app_discovery_service/almanac_fetcher.cc

// Copyright 2023 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/apps/app_discovery_service/almanac_fetcher.h"

#include <vector>

#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/apps/almanac_api_client/almanac_icon_cache.h"
#include "chrome/browser/apps/app_discovery_service/almanac_api/launcher_app.pb.h"
#include "chrome/browser/apps/app_discovery_service/app_discovery_service.h"
#include "chrome/browser/apps/app_discovery_service/game_extras.h"
#include "chrome/browser/apps/app_discovery_service/launcher_app_almanac_endpoint.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "google_apis/google_api_keys.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "url/gurl.h"

namespace apps {
namespace {
// Relative file path to where the launcher app data will be stored on disk.
constexpr char kLauncherAppFilePath[] =
    "app_discovery_service/launcher_app/data.pb";
// Profile preference used to track the last time the Almanac was called for the
// Launcher App use case.
constexpr char kLastLauncherAppAlmanacCallTimestamp[] =
    "app_discovery_service.last_launcher_app_almanac_call_timestamp";

// Whether or not to skip the check if the build includes the Google Chrome API
// key. Used for testing.
bool skip_api_key_check_for_testing = false;

// Maps the Almanac Launcher App proto response to an app result. The icon
// url is passed and later handled by the icon cache.
std::vector<Result> MapToApps(const proto::LauncherAppResponse& proto) {
  std::vector<Result> apps;
  for (proto::LauncherAppResponse::AppGroup app_group : proto.app_groups()) {
    // Skip apps we cannot display.
    if (app_group.icons().empty() || app_group.name().empty() ||
        app_group.action_link().empty()) {
      continue;
    }
    // There should be just a single app with a single icon. We want to
    // handle more in the future but for now just read the first icon.
    const proto::LauncherAppResponse::Icon& icon = app_group.icons(0);
    auto extras = std::make_unique<GameExtras>(
        base::UTF8ToUTF16(app_group.badge_text()),
        /*relative_icon_path_=*/base::FilePath(""), icon.is_masking_allowed(),
        GURL(app_group.action_link()));

    apps.emplace_back(AppSource::kGames, icon.url(),
                      base::UTF8ToUTF16(app_group.name()), std::move(extras));
  }
  return apps;
}

// Handles the downloaded image and converts it to the right format.
void OnIconDownloaded(GetIconCallback callback, const gfx::Image& icon) {
  DiscoveryError status = DiscoveryError::kSuccess;
  if (icon.IsEmpty()) {
    status = DiscoveryError::kErrorRequestFailed;
  }
  std::move(callback).Run(icon.AsImageSkia(), status);
}
}  // namespace

AlmanacFetcher::AlmanacFetcher(Profile* profile,
                               std::unique_ptr<AlmanacIconCache> icon_cache)
    : profile_(profile),
      device_info_manager_(std::make_unique<DeviceInfoManager>(profile)),
      icon_cache_(std::move(icon_cache)) {
  // The whole feature would not work unless the build includes the Google
  // Chrome API key or this is a test environment as the server call would fail.
  if ((google_apis::IsGoogleChromeAPIKeyUsed() ||
       skip_api_key_check_for_testing) &&
      chromeos::features::IsCloudGamingDeviceEnabled()) {
    base::FilePath path = profile->GetPath().AppendASCII(kLauncherAppFilePath);
    proto_file_manager_ =
        std::make_unique<ProtoFileManager<proto::LauncherAppResponse>>(path);
    DownloadApps();
  }
}

AlmanacFetcher::~AlmanacFetcher() = default;

void AlmanacFetcher::GetApps(ResultCallback callback) {
  auto error = apps_.empty() ? DiscoveryError::kErrorRequestFailed
                             : DiscoveryError::kSuccess;
  std::move(callback).Run(apps_, error);
}

base::CallbackListSubscription AlmanacFetcher::RegisterForAppUpdates(
    RepeatingResultCallback callback) {
  if (!apps_.empty()) {
    callback.Run(apps_);
  }
  return subscribers_.Add(std::move(callback));
}

// Method calls the icon cache for the given url.
void AlmanacFetcher::GetIcon(const std::string& icon_id,
                             int32_t size_hint_in_dip,
                             GetIconCallback callback) {
  // Do not use the icon cache if the environment isn't setup correctly.
  if (!icon_cache_ || apps_.empty()) {
    std::move(callback).Run(gfx::ImageSkia(),
                            DiscoveryError::kErrorRequestFailed);
    return;
  }
  // We ignore the size as it's hard-coded to kAppIconDimension in:
  // //chrome/browser/ash/app_list/search/common/icon_constants.h
  icon_cache_->GetIcon(GURL(icon_id),
                       base::BindOnce(&OnIconDownloaded, std::move(callback)));
}

void AlmanacFetcher::OnAppsUpdate(
    std::optional<proto::LauncherAppResponse> response) {
  if (!response.has_value()) {
    return;
  }
  apps_ = MapToApps(*response);
  subscribers_.Notify(apps_);
}

void AlmanacFetcher::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterTimePref(kLastLauncherAppAlmanacCallTimestamp,
                             base::Time());
}

void AlmanacFetcher::SetSkipApiKeyCheckForTesting(bool skip_api_key_check) {
  skip_api_key_check_for_testing = skip_api_key_check;
}

void AlmanacFetcher::DownloadApps() {
  if ((base::Time::Now() - GetLastAppsUpdateTime()).InHours() >= 24) {
    device_info_manager_->GetDeviceInfo(base::BindOnce(
        &AlmanacFetcher::OnGetDeviceInfo, weak_factory_.GetWeakPtr()));
  } else {
    proto_file_manager_->ReadProtoFromFile(base::BindOnce(
        &AlmanacFetcher::OnAppsUpdate, weak_factory_.GetWeakPtr()));
  }
}

void AlmanacFetcher::OnGetDeviceInfo(DeviceInfo device_info) {
  launcher_app_almanac_endpoint::GetApps(
      device_info, *profile_->GetURLLoaderFactory(),
      base::BindOnce(&AlmanacFetcher::OnServerResponse,
                     weak_factory_.GetWeakPtr()));
}

void AlmanacFetcher::OnServerResponse(
    std::optional<proto::LauncherAppResponse> response) {
  if (response.has_value()) {
    proto_file_manager_->WriteProtoToFile(
        *response, base::BindOnce(&AlmanacFetcher::OnFileWritten,
                                  weak_factory_.GetWeakPtr(), *response));
  } else {
    proto_file_manager_->ReadProtoFromFile(base::BindOnce(
        &AlmanacFetcher::OnAppsUpdate, weak_factory_.GetWeakPtr()));
  }
}

void AlmanacFetcher::OnFileWritten(proto::LauncherAppResponse response,
                                   bool write_complete) {
  OnAppsUpdate(std::move(response));
  if (!write_complete) {
    LOG(ERROR) << "Writing server response to disk failed";
    return;
  }
  // Only set if writing data to disk succeeded.
  SetLastAppsUpdateTime(base::Time::Now());
}

base::Time AlmanacFetcher::GetLastAppsUpdateTime() const {
  return profile_->GetPrefs()->GetTime(kLastLauncherAppAlmanacCallTimestamp);
}

void AlmanacFetcher::SetLastAppsUpdateTime(base::Time value) {
  profile_->GetPrefs()->SetTime(kLastLauncherAppAlmanacCallTimestamp, value);
}
}  // namespace apps