// Copyright 2018 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/recommended_arc_apps/recommend_apps_fetcher_impl.h"
#include <string_view>
#include "base/base64url.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/apps/app_discovery_service/recommended_arc_apps/recommend_apps_fetcher_delegate.h"
#include "content/public/browser/gpu_data_manager.h"
#include "extensions/common/api/system_display.h"
#include "gpu/config/gpu_info.h"
#include "net/base/load_flags.h"
#include "net/http/http_status_code.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 "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/zlib/google/compression_utils.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/events/devices/input_device.h"
#include "ui/gfx/extension_set.h"
#include "ui/gl/gl_version_info.h"
namespace apps {
namespace {
constexpr const char kGetRevisedAppListUrl[] =
"https://android.clients.google.com/fdfe/chrome/getSetupAppRecommendations";
constexpr base::TimeDelta kDownloadTimeOut = base::Minutes(1);
constexpr const int64_t kMaxDownloadBytes = 1024 * 1024; // 1Mb
// Fake gpu info for test.
const gpu::GPUInfo* g_gpu_info_for_test = nullptr;
bool HasTouchScreen() {
return !ui::DeviceDataManager::GetInstance()->GetTouchscreenDevices().empty();
}
bool HasStylusInput() {
// Check to see if the hardware reports it is stylus capable.
for (const ui::TouchscreenDevice& device :
ui::DeviceDataManager::GetInstance()->GetTouchscreenDevices()) {
if (device.has_stylus &&
device.type == ui::InputDeviceType::INPUT_DEVICE_INTERNAL) {
return true;
}
}
return false;
}
bool HasKeyboard() {
return !ui::DeviceDataManager::GetInstance()->GetKeyboardDevices().empty();
}
bool HasHardKeyboard() {
for (const ui::InputDevice& device :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
if (!device.phys.empty()) {
return true;
}
}
return false;
}
gfx::Size GetScreenSize() {
return display::Screen::GetScreen()->GetPrimaryDisplay().GetSizeInPixel();
}
// TODO(rsgingerrs): This function is copied from Play. We need to find a way to
// keep this synced with the Play side if there are any changes there. Another
// approach is to let the server do the calculation since we have provided the
// screen width, height and dpi.
int CalculateStableScreenLayout(const int screen_width,
const int screen_height,
const float dpi) {
const int density_default = 160;
const float px_to_dp = density_default / static_cast<float>(dpi);
const int screen_width_dp = static_cast<int>(screen_width * px_to_dp);
const int screen_height_dp = static_cast<int>(screen_height * px_to_dp);
const int short_size_dp = std::min(screen_width_dp, screen_height_dp);
const int long_size_dp = std::max(screen_width_dp, screen_height_dp);
int screen_layout_size;
bool screen_layout_long;
const int screenlayout_size_small = 0x01;
const int screenlayout_size_normal = 0x02;
const int screenlayout_size_large = 0x03;
const int screenlayout_size_xlarge = 0x04;
const int screenlayout_long_no = 0x10;
// These semi-magic numbers define our compatibility modes for
// applications with different screens. These are guarantees to
// app developers about the space they can expect for a particular
// configuration. DO NOT CHANGE!
if (long_size_dp < 470) {
// This is shorter than an HVGA normal density screen (which
// is 480 pixels on its long side).
screen_layout_size = screenlayout_size_small;
screen_layout_long = false;
} else {
// What size is this screen?
if (long_size_dp >= 960 && short_size_dp >= 720) {
// 1.5xVGA or larger screens at medium density are the point
// at which we consider it to be an extra large screen.
screen_layout_size = screenlayout_size_xlarge;
} else if (long_size_dp >= 640 && short_size_dp >= 480) {
// VGA or larger screens at medium density are the point
// at which we consider it to be a large screen.
screen_layout_size = screenlayout_size_large;
} else {
screen_layout_size = screenlayout_size_normal;
}
// Is this a long screen? Anything wider than WVGA (5:3) is considering to
// be long.
screen_layout_long = ((long_size_dp * 3) / 5) >= (short_size_dp - 1);
}
int screen_layout = screen_layout_size;
if (!screen_layout_long) {
screen_layout |= screenlayout_long_no;
}
return screen_layout;
}
device_configuration::DeviceConfigurationProto_ScreenLayout
GetScreenLayoutSizeId(const int screen_layout_size_value) {
const int screenlayout_size_small = 0x01;
const int screenlayout_size_normal = 0x02;
const int screenlayout_size_large = 0x03;
const int screenlayout_size_xlarge = 0x04;
const int screenlayout_size_mask = 0x0f;
int size_bits = screen_layout_size_value & screenlayout_size_mask;
switch (size_bits) {
case screenlayout_size_small:
return device_configuration::DeviceConfigurationProto_ScreenLayout::
DeviceConfigurationProto_ScreenLayout_SMALL;
case screenlayout_size_normal:
return device_configuration::DeviceConfigurationProto_ScreenLayout::
DeviceConfigurationProto_ScreenLayout_NORMAL;
case screenlayout_size_large:
return device_configuration::DeviceConfigurationProto_ScreenLayout::
DeviceConfigurationProto_ScreenLayout_LARGE;
case screenlayout_size_xlarge:
return device_configuration::DeviceConfigurationProto_ScreenLayout::
DeviceConfigurationProto_ScreenLayout_EXTRA_LARGE;
default:
return device_configuration::DeviceConfigurationProto_ScreenLayout::
DeviceConfigurationProto_ScreenLayout_UNDEFINED_SCREEN_LAYOUT;
}
}
const gpu::GPUInfo GetGPUInfo() {
if (g_gpu_info_for_test) {
return *g_gpu_info_for_test;
}
return content::GpuDataManager::GetInstance()->GetGPUInfo();
}
// This function converts the major and minor versions to the proto accepted
// value. For example, if the version is 3.2, the return value is 0x00030002.
unsigned GetGLVersionInfo(const gpu::GPUInfo& gpu_info) {
gfx::ExtensionSet extensionSet(gfx::MakeExtensionSet(gpu_info.gl_extensions));
gl::GLVersionInfo glVersionInfo(gpu_info.gl_version.c_str(),
gpu_info.gl_renderer.c_str(), extensionSet);
unsigned major_version = glVersionInfo.major_version;
unsigned minor_version = glVersionInfo.minor_version;
unsigned version = 0x0000ffff;
version &= minor_version;
version |= (major_version << 16) & 0xffff0000;
return version;
}
gfx::ExtensionSet GetGLExtensions(const gpu::GPUInfo& gpu_info) {
gfx::ExtensionSet extensionSet(gfx::MakeExtensionSet(gpu_info.gl_extensions));
return extensionSet;
}
const std::string& GetDeviceFingerprint(const arc::ArcFeatures& arc_features) {
return arc_features.build_props.fingerprint;
}
const std::string& GetAndroidSdkVersion(const arc::ArcFeatures& arc_features) {
return arc_features.build_props.sdk_version;
}
std::vector<std::string> GetCpuAbiList(const arc::ArcFeatures& arc_features) {
// The property value will be a comma separated list, e.g. "x86_64,x86".
return base::SplitString(arc_features.build_props.abi_list, ",",
base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
}
std::string CompressAndEncodeProtoMessageOnBlockingThread(
device_configuration::DeviceConfigurationProto device_config) {
std::string encoded_device_configuration_proto;
std::string serialized_proto;
device_config.SerializeToString(&serialized_proto);
std::string compressed_proto;
compression::GzipCompress(serialized_proto, &compressed_proto);
base::Base64UrlEncode(compressed_proto,
base::Base64UrlEncodePolicy::OMIT_PADDING,
&encoded_device_configuration_proto);
return encoded_device_configuration_proto;
}
void RecordUmaDownloadTime(base::TimeDelta download_time) {
UMA_HISTOGRAM_TIMES("OOBE.RecommendApps.Fetcher.DownloadTime", download_time);
}
void RecordUmaResponseCode(int code) {
base::UmaHistogramSparse("OOBE.RecommendApps.Fetcher.ResponseCode", code);
}
} // namespace
RecommendAppsFetcherImpl::ScopedGpuInfoForTest::ScopedGpuInfoForTest(
const gpu::GPUInfo* gpu_info) {
DCHECK(!g_gpu_info_for_test);
g_gpu_info_for_test = gpu_info;
}
RecommendAppsFetcherImpl::ScopedGpuInfoForTest::~ScopedGpuInfoForTest() {
g_gpu_info_for_test = nullptr;
}
RecommendAppsFetcherImpl::RecommendAppsFetcherImpl(
RecommendAppsFetcherDelegate* delegate,
mojo::PendingRemote<crosapi::mojom::CrosDisplayConfigController>
display_config,
network::mojom::URLLoaderFactory* url_loader_factory)
: delegate_(delegate),
url_loader_factory_(url_loader_factory),
arc_features_getter_(
base::BindRepeating(&arc::ArcFeaturesParser::GetArcFeatures)),
cros_display_config_(std::move(display_config)) {}
RecommendAppsFetcherImpl::~RecommendAppsFetcherImpl() = default;
void RecommendAppsFetcherImpl::PopulateDeviceConfig() {
if (!HasTouchScreen()) {
device_config_.set_touch_screen(
device_configuration::DeviceConfigurationProto_TouchScreen::
DeviceConfigurationProto_TouchScreen_NOTOUCH);
} else if (!HasStylusInput()) {
device_config_.set_touch_screen(
device_configuration::DeviceConfigurationProto_TouchScreen::
DeviceConfigurationProto_TouchScreen_FINGER);
} else {
device_config_.set_touch_screen(
device_configuration::DeviceConfigurationProto_TouchScreen::
DeviceConfigurationProto_TouchScreen_STYLUS);
}
if (!HasKeyboard()) {
device_config_.set_keyboard(
device_configuration::DeviceConfigurationProto_Keyboard::
DeviceConfigurationProto_Keyboard_NOKEYS);
} else {
// TODO(rsgingerrs): Currently there is no straightforward way to determine
// whether it is a full keyboard or not. We assume it is safe to set it as
// QWERTY keyboard for this feature.
device_config_.set_keyboard(
device_configuration::DeviceConfigurationProto_Keyboard::
DeviceConfigurationProto_Keyboard_QWERTY);
}
device_config_.set_has_hard_keyboard(HasHardKeyboard());
// TODO(rsgingerrs): There is no straightforward way to get this info. We
// assume it is safe to set it as no navigation.
device_config_.set_navigation(
device_configuration::DeviceConfigurationProto_Navigation::
DeviceConfigurationProto_Navigation_NONAV);
device_config_.set_has_five_way_navigation(false);
const gpu::GPUInfo gpu_info = GetGPUInfo();
device_config_.set_gl_es_version(GetGLVersionInfo(gpu_info));
for (std::string_view gl_extension : GetGLExtensions(gpu_info)) {
if (!gl_extension.empty()) {
device_config_.add_gl_extension(std::string(gl_extension));
}
}
}
void RecommendAppsFetcherImpl::StartAshRequest() {
cros_display_config_->GetDisplayUnitInfoList(
false /* single_unified */,
base::BindOnce(&RecommendAppsFetcherImpl::OnAshResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void RecommendAppsFetcherImpl::MaybeStartCompressAndEncodeProtoMessage() {
if (!ash_ready_ || !arc_features_ready_ || has_started_proto_processing_) {
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&CompressAndEncodeProtoMessageOnBlockingThread,
std::move(device_config_)),
base::BindOnce(
&RecommendAppsFetcherImpl::OnProtoMessageCompressedAndEncoded,
weak_ptr_factory_.GetWeakPtr()));
has_started_proto_processing_ = true;
}
void RecommendAppsFetcherImpl::OnProtoMessageCompressedAndEncoded(
std::string encoded_device_configuration_proto) {
proto_compressed_and_encoded_ = true;
encoded_device_configuration_proto_ = encoded_device_configuration_proto;
StartDownload();
}
void RecommendAppsFetcherImpl::OnAshResponse(
std::vector<crosapi::mojom::DisplayUnitInfoPtr> all_displays_info) {
ash_ready_ = true;
int screen_density = 0;
for (const crosapi::mojom::DisplayUnitInfoPtr& display_info :
all_displays_info) {
if (base::NumberToString(display::Display::InternalDisplayId()) ==
display_info->id) {
screen_density = display_info->dpi_x + display_info->dpi_y;
break;
}
}
device_config_.set_screen_density(screen_density);
const int screen_width = GetScreenSize().width();
const int screen_height = GetScreenSize().height();
device_config_.set_screen_width(screen_width);
device_config_.set_screen_height(screen_height);
const int screen_layout =
CalculateStableScreenLayout(screen_width, screen_height, screen_density);
device_config_.set_screen_layout(GetScreenLayoutSizeId(screen_layout));
MaybeStartCompressAndEncodeProtoMessage();
}
void RecommendAppsFetcherImpl::OnArcFeaturesRead(
std::optional<arc::ArcFeatures> read_result) {
arc_features_ready_ = true;
if (read_result != std::nullopt) {
for (const auto& feature : read_result.value().feature_map) {
device_config_.add_system_available_feature(feature.first);
}
for (const auto& abi : GetCpuAbiList(read_result.value())) {
device_config_.add_native_platform(abi);
}
play_store_version_ = read_result.value().play_store_version;
android_sdk_version_ = GetAndroidSdkVersion(read_result.value());
device_fingerprint_ = GetDeviceFingerprint(read_result.value());
}
MaybeStartCompressAndEncodeProtoMessage();
}
void RecommendAppsFetcherImpl::StartDownload() {
if (!proto_compressed_and_encoded_) {
return;
}
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("play_recommended_apps_download", R"(
semantics {
sender: "ChromeOS Recommended Apps Screen"
description:
"Chrome OS downloads the recommended app list from Google Play API."
trigger:
"When user has accepted the ARC Terms of Service."
data:
"URL of the Google Play API."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: YES
setting:
"NA"
policy_exception_justification:
"Not implemented, considered not necessary."
})");
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = GURL(kGetRevisedAppListUrl);
resource_request->headers.SetHeader("X-DFE-Device-Fingerprint",
device_fingerprint_);
resource_request->method = "GET";
resource_request->load_flags =
net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE;
resource_request->headers.SetHeader("X-DFE-Device-Config",
encoded_device_configuration_proto_);
resource_request->headers.SetHeader("X-DFE-Sdk-Version",
android_sdk_version_);
resource_request->headers.SetHeader("X-DFE-Chromesky-Client-Version",
play_store_version_);
start_time_ = base::TimeTicks::Now();
app_list_loader_ = network::SimpleURLLoader::Create(
std::move(resource_request), traffic_annotation);
// Retry up to three times if network changes are detected during the
// download.
app_list_loader_->SetRetryOptions(
3, network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
app_list_loader_->DownloadToString(
url_loader_factory_,
base::BindOnce(&RecommendAppsFetcherImpl::OnDownloaded,
base::Unretained(this)),
kMaxDownloadBytes);
// Abort the download attempt if it takes longer than one minute.
download_timer_.Start(FROM_HERE, kDownloadTimeOut, this,
&RecommendAppsFetcherImpl::OnDownloadTimeout);
}
void RecommendAppsFetcherImpl::OnDownloadTimeout() {
// Destroy the fetcher, which will abort the download attempt.
app_list_loader_.reset();
RecordUmaDownloadTime(base::TimeTicks::Now() - start_time_);
delegate_->OnLoadError();
}
void RecommendAppsFetcherImpl::OnDownloaded(
std::unique_ptr<std::string> response_body) {
download_timer_.Stop();
RecordUmaDownloadTime(base::TimeTicks::Now() - start_time_);
std::unique_ptr<network::SimpleURLLoader> loader(std::move(app_list_loader_));
int response_code = 0;
if (!loader->ResponseInfo() || !loader->ResponseInfo()->headers) {
delegate_->OnLoadError();
return;
}
response_code = loader->ResponseInfo()->headers->response_code();
RecordUmaResponseCode(response_code);
// If the recommended app list could not be downloaded, show an error message
// to the user.
if (!response_body || response_body->empty()) {
delegate_->OnLoadError();
return;
}
// If the recommended app list were downloaded successfully, show them to
// the user.
//
// The response starts with a prefix ")]}'". This needs to be removed before
// further parsing.
const std::string json_xss_prevention_prefix = ")]}'";
std::string response_body_json = *response_body;
if (base::StartsWith(response_body_json, json_xss_prevention_prefix)) {
response_body_json =
response_body_json.substr(json_xss_prevention_prefix.length());
}
data_decoder::DataDecoder::ParseJsonIsolated(
response_body_json,
base::BindOnce(&RecommendAppsFetcherImpl::OnJsonParsed,
weak_ptr_factory_.GetWeakPtr()));
}
void RecommendAppsFetcherImpl::Start() {
PopulateDeviceConfig();
StartAshRequest();
arc_features_getter_.Run(
base::BindOnce(&RecommendAppsFetcherImpl::OnArcFeaturesRead,
weak_ptr_factory_.GetWeakPtr()));
}
void RecommendAppsFetcherImpl::Retry() {
StartDownload();
}
void RecommendAppsFetcherImpl::OnJsonParsed(
data_decoder::DataDecoder::ValueOrError result) {
if (!result.has_value()) {
delegate_->OnParseResponseError();
return;
}
delegate_->OnLoadSuccess(std::move(*result));
}
} // namespace apps