// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chrome/browser/ash/sparky/sparky_delegate_impl.h"
#include <map>
#include <memory>
#include <optional>
#include <string>
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/window_tree_host_lookup.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/time_formatting.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/time/time.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/file_manager/open_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/trash_common_util.h"
#include "chrome/browser/ash/sparky/keyboard_util.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/ash/components/sparky/sparky_util.h"
#include "components/manta/sparky/sparky_delegate.h"
#include "components/manta/sparky/system_info_delegate.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/l10n/time_format.h"
#include "ui/base/text/bytes_formatting.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/types/display_constants.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
using SetPrefResult = extensions::settings_private::SetPrefResult;
using SettingsPrivatePrefType = extensions::api::settings_private::PrefType;
// Returns a displayable time for the last modified date.
std::u16string GetFormattedTime(base::Time time) {
std::u16string date_time_of_day = base::TimeFormatTimeOfDay(time);
std::u16string relative_date = ui::TimeFormat::RelativeDate(time, nullptr);
std::u16string formatted_time;
if (!relative_date.empty()) {
relative_date = base::ToLowerASCII(relative_date);
formatted_time = relative_date + u" " + date_time_of_day;
} else {
formatted_time = base::TimeFormatShortDate(time) + u", " + date_time_of_day;
}
return formatted_time;
}
// Returns a vector of Files within the root file path.
std::vector<manta::FileData> SearchFiles(
const base::FilePath& my_files_path,
const std::vector<base::FilePath> trash_paths,
bool obtain_bytes,
std::set<std::string> allowed_file_paths) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
// Enumerate through all of the files in the My Files folder.
std::vector<manta::FileData> files_data;
base::FileEnumerator file_enumerator(my_files_path,
/*recursive=*/true,
base::FileEnumerator::FileType::FILES);
for (base::FilePath file_path = file_enumerator.Next(); !file_path.empty();
file_path = file_enumerator.Next()) {
// Exclude any paths that are parented at an enabled trash location.
if (base::ranges::any_of(trash_paths,
[&file_path](const base::FilePath& trash_path) {
return trash_path.IsParent(file_path);
})) {
continue;
}
// Get the file's name.
std::string file_name = file_path.BaseName().AsUTF8Unsafe();
// If a set of allowed file paths is defined, then only include files within
// this list.
if (!allowed_file_paths.empty() &&
!allowed_file_paths.contains(file_path.AsUTF8Unsafe())) {
continue;
}
// Open the file.
base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
// Get the file's information.
base::Time file_date_modified =
base::Time::FromTimeT(file_enumerator.GetInfo().stat().st_atime);
std::string file_date_modified_string =
base::UTF16ToUTF8(GetFormattedTime(file_date_modified));
auto file_data = manta::FileData(file_path.AsUTF8Unsafe(), file_name,
file_date_modified_string);
// Obtain the bytes of the file if requested.
if (obtain_bytes) {
file_data.bytes = base::ReadFileToBytes(file_path);
}
file_data.size_in_bytes = file_enumerator.GetInfo().GetSize();
// Create a `FilesData` object.
files_data.emplace_back(std::move(file_data));
}
return files_data;
}
} // namespace
SparkyDelegateImpl::SparkyDelegateImpl(Profile* profile)
: profile_(profile),
prefs_util_(std::make_unique<extensions::PrefsUtil>(profile)),
screenshot_handler_(std::make_unique<sparky::ScreenshotHandler>()),
total_disk_space_calculator_(profile),
free_disk_space_calculator_(profile),
root_path_(file_manager::util::GetMyFilesFolderForProfile(profile_)) {
StartObservingCalculators();
}
SparkyDelegateImpl::~SparkyDelegateImpl() {
StopObservingCalculators();
}
bool SparkyDelegateImpl::SetSettings(
std::unique_ptr<manta::SettingsData> settings_data) {
if (!settings_data->val_set) {
return false;
}
if (settings_data->pref_name == prefs::kDarkModeEnabled) {
profile_->GetPrefs()->SetBoolean(settings_data->pref_name,
settings_data->bool_val);
return true;
}
SetPrefResult result = prefs_util_->SetPref(
settings_data->pref_name, base::to_address(settings_data->GetValue()));
return result == SetPrefResult::SUCCESS;
}
void SparkyDelegateImpl::AddPrefToMap(
const std::string& pref_name,
SettingsPrivatePrefType settings_pref_type,
std::optional<base::Value> value) {
// TODO (b:354608065) Add in UMA logging for these error cases.
switch (settings_pref_type) {
case SettingsPrivatePrefType::kBoolean: {
if (!value->is_bool()) {
DVLOG(1) << "Cros setting " << pref_name
<< " has a prefType of bool, but has a value of type: "
<< value->type();
break;
}
current_prefs_[pref_name] = std::make_unique<manta::SettingsData>(
pref_name, manta::PrefType::kBoolean, std::move(value));
break;
}
case SettingsPrivatePrefType::kNumber: {
if (value->is_int()) {
current_prefs_[pref_name] = std::make_unique<manta::SettingsData>(
pref_name, manta::PrefType::kInt, std::move(value));
} else if (value->is_double()) {
current_prefs_[pref_name] = std::make_unique<manta::SettingsData>(
pref_name, manta::PrefType::kDouble, std::move(value));
} else {
DVLOG(1) << "Cros setting " << pref_name
<< " has a prefType of number, but has a value of type: "
<< value->type();
}
break;
}
case SettingsPrivatePrefType::kList: {
if (!value->is_list()) {
DVLOG(1) << "Cros setting " << pref_name
<< " has a prefType of list, but has a value of type: "
<< value->type();
break;
}
current_prefs_[pref_name] = std::make_unique<manta::SettingsData>(
pref_name, manta::PrefType::kList, std::move(value));
break;
}
case SettingsPrivatePrefType::kString:
case SettingsPrivatePrefType::kUrl: {
if (!value->is_string()) {
DVLOG(1)
<< "Cros setting " << pref_name
<< " has a prefType of string or url, but has a value of type: "
<< value->type();
break;
}
current_prefs_[pref_name] = std::make_unique<manta::SettingsData>(
pref_name, manta::PrefType::kString, std::move(value));
break;
}
case SettingsPrivatePrefType::kDictionary: {
if (!value->is_dict()) {
DVLOG(1) << "Cros setting " << pref_name
<< " has a prefType of dictionary, but has a value of type: "
<< value->type();
break;
}
current_prefs_[pref_name] = std::make_unique<manta::SettingsData>(
pref_name, manta::PrefType::kDictionary, std::move(value));
break;
}
default:
break;
}
}
SparkyDelegateImpl::SettingsDataList* SparkyDelegateImpl::GetSettingsList() {
extensions::PrefsUtil::TypedPrefMap pref_list =
prefs_util_->GetAllowlistedKeys();
current_prefs_ = SparkyDelegateImpl::SettingsDataList();
for (auto const& [pref_name, pref_type] : pref_list) {
auto pref_object = prefs_util_->GetPref(pref_name);
if (pref_object.has_value()) {
AddPrefToMap(pref_name, pref_type, std::move(pref_object->value));
}
}
current_prefs_[prefs::kDarkModeEnabled] =
std::make_unique<manta::SettingsData>(
prefs::kDarkModeEnabled, manta::PrefType::kBoolean,
std::make_optional<base::Value>(
profile_->GetPrefs()->GetBoolean(prefs::kDarkModeEnabled)));
return ¤t_prefs_;
}
std::optional<base::Value> SparkyDelegateImpl::GetSettingValue(
const std::string& setting_id) {
if (setting_id == prefs::kDarkModeEnabled) {
return std::make_optional<base::Value>(
profile_->GetPrefs()->GetBoolean(prefs::kDarkModeEnabled));
}
auto pref_object = prefs_util_->GetPref(setting_id);
if (pref_object.has_value()) {
return std::move(pref_object->value);
} else {
return std::nullopt;
}
}
void SparkyDelegateImpl::GetScreenshot(manta::ScreenshotDataCallback callback) {
screenshot_handler_->TakeScreenshot(std::move(callback));
}
std::vector<manta::AppsData> SparkyDelegateImpl::GetAppsList() {
std::vector<manta::AppsData> apps;
apps::AppServiceProxyFactory::GetForProfile(profile_)
->AppRegistryCache()
.ForEachApp([&apps](const apps::AppUpdate& update) {
if (!apps_util::IsInstalled(update.Readiness())) {
return;
}
if (!update.ShowInSearch().value_or(false) &&
!(update.Recommendable().value_or(false) &&
update.AppType() == apps::AppType::kBuiltIn)) {
return;
}
manta::AppsData& app = apps.emplace_back(update.Name(), update.AppId());
for (const std::string& term : update.AdditionalSearchTerms()) {
app.AddSearchableText(term);
}
});
return apps;
}
void SparkyDelegateImpl::LaunchApp(const std::string& app_id) {
apps::AppServiceProxy* proxy =
apps::AppServiceProxyFactory::GetForProfile(profile_);
proxy->Launch(app_id, ui::EF_IS_SYNTHESIZED, apps::LaunchSource::kFromSparky,
std::make_unique<apps::WindowInfo>(display::kDefaultDisplayId));
}
void SparkyDelegateImpl::ObtainStorageInfo(
manta::StorageDataCallback storage_callback) {
storage_callback_ = std::move(storage_callback);
total_disk_space_calculator_.StartCalculation();
free_disk_space_calculator_.StartCalculation();
}
void SparkyDelegateImpl::Click(int x, int y) {
// Get the Window of the primary display.
const auto& display = display::Screen::GetScreen()->GetPrimaryDisplay();
auto* host = ash::GetWindowTreeHostForDisplay(display.id());
CHECK(host);
aura::Window* window = host->window();
CHECK(window);
// Create a point in window coordinates, which can be different from screen
// coordinates if multiple screens are present.
gfx::Point point(x, y);
::wm::ConvertPointFromScreen(window, &point);
// Create a pair of pressed/released mouse events. These need to be scaled to
// the screen to account for non-1x scale factors.
ui::MouseEvent mouse_pressed(ui::EventType::kMousePressed, point, point,
ui::EventTimeForNow(), ui::EF_IS_SYNTHESIZED,
ui::EF_LEFT_MOUSE_BUTTON);
ui::MouseEvent mouse_released(ui::EventType::kMouseReleased, point, point,
ui::EventTimeForNow(), ui::EF_IS_SYNTHESIZED,
ui::EF_LEFT_MOUSE_BUTTON);
mouse_pressed.UpdateForRootTransform(
host->GetRootTransform(),
host->GetRootTransformForLocalEventCoordinates());
mouse_released.UpdateForRootTransform(
host->GetRootTransform(),
host->GetRootTransformForLocalEventCoordinates());
// Other parts of the system can temporarily disable mouse events. If this is
// the case, re-enable them for the duration of our calls.
auto* cursor = aura::client::GetCursorClient(window);
const bool mouse_disabled = !cursor->IsMouseEventsEnabled();
if (mouse_disabled) {
cursor->EnableMouseEvents();
}
// No delay is needed between these events.
//
// DeliverEventToSink skips event rewriters, unlike SendEventToSink.
// TODO(b/351099209): understand if this is desirable.
host->DeliverEventToSink(&mouse_pressed);
host->DeliverEventToSink(&mouse_released);
if (mouse_disabled) {
cursor->DisableMouseEvents();
}
}
void SparkyDelegateImpl::KeyboardEntry(std::string text) {
// Get the window tree host for the primary display.
const auto& display = display::Screen::GetScreen()->GetPrimaryDisplay();
auto* host = ash::GetWindowTreeHostForDisplay(display.id());
CHECK(host);
auto key_events = KeyEventsForText(text);
if (!key_events) {
// TODO(b/351099209): report an error, `text` contains non-typeable
// characters.
return;
}
for (auto& key_event : key_events.value()) {
host->DeliverEventToSink(&key_event);
}
}
void SparkyDelegateImpl::KeyPress(const std::string& key,
bool control,
bool alt,
bool shift) {
// Get the window tree host for the primary display.
const auto& display = display::Screen::GetScreen()->GetPrimaryDisplay();
auto* host = ash::GetWindowTreeHostForDisplay(display.id());
CHECK(host);
const auto key_code = KeyboardCodeForDOMString(key);
if (key_code) {
auto pressed_released =
MakeKeyEventPair(key_code.value(), control, alt, shift);
host->DeliverEventToSink(&pressed_released.first);
host->DeliverEventToSink(&pressed_released.second);
} else {
// TODO(b/351099209): Report an error.
}
}
void SparkyDelegateImpl::StartObservingCalculators() {
total_disk_space_calculator_.AddObserver(this);
free_disk_space_calculator_.AddObserver(this);
}
void SparkyDelegateImpl::StopObservingCalculators() {
total_disk_space_calculator_.RemoveObserver(this);
free_disk_space_calculator_.RemoveObserver(this);
}
void SparkyDelegateImpl::OnSizeCalculated(
const SimpleSizeCalculator::CalculationType& calculation_type,
int64_t total_bytes) {
// The total disk space is rounded to the next power of 2.
if (calculation_type == SimpleSizeCalculator::CalculationType::kTotal) {
total_bytes = sparky::RoundByteSize(total_bytes);
}
// Store calculated item's size.
const int item_index = static_cast<int>(calculation_type);
storage_items_total_bytes_[item_index] = total_bytes;
// Mark item as calculated.
calculation_state_.set(item_index);
OnStorageInfoUpdated();
}
void SparkyDelegateImpl::OnStorageInfoUpdated() {
// If some size calculations are pending, return early and wait for all
// calculations to complete.
if (!calculation_state_.all()) {
return;
}
const int total_space_index =
static_cast<int>(SimpleSizeCalculator::CalculationType::kTotal);
const int free_disk_space_index =
static_cast<int>(SimpleSizeCalculator::CalculationType::kAvailable);
int64_t total_bytes = storage_items_total_bytes_[total_space_index];
int64_t available_bytes = storage_items_total_bytes_[free_disk_space_index];
if (total_bytes <= 0 || available_bytes < 0) {
// We can't get useful information from the storage page if total_bytes <=
// 0 or available_bytes is less than 0. This is not expected to happen.
NOTREACHED_IN_MIGRATION()
<< "Unable to retrieve total or available disk space";
return;
}
std::move(storage_callback_)
.Run(std::make_unique<manta::StorageData>(
base::UTF16ToUTF8(ui::FormatBytes(available_bytes)),
base::UTF16ToUTF8(ui::FormatBytes(total_bytes))));
}
void SparkyDelegateImpl::LaunchFile(const std::string& file_path) {
file_manager::util::OpenItem(profile_, base::FilePath(file_path),
platform_util::OpenItemType::OPEN_FILE,
base::DoNothing());
}
void SparkyDelegateImpl::GetMyFiles(manta::FilesDataCallback callback,
bool obtain_bytes,
std::set<std::string> allowed_file_paths) {
if (trash_paths_.empty()) {
if (!file_manager::trash::IsTrashEnabledForProfile(profile_)) {
trash_paths_ = std::vector<base::FilePath>();
} else {
auto enabled_trash_locations =
file_manager::trash::GenerateEnabledTrashLocationsForProfile(
profile_, /*base_path=*/base::FilePath());
for (const auto& it : enabled_trash_locations) {
trash_paths_.emplace_back(
it.first.Append(it.second.relative_folder_path));
}
}
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
base::BindOnce(SearchFiles, root_path_, trash_paths_, obtain_bytes,
allowed_file_paths),
std::move(callback));
}
void SparkyDelegateImpl::UpdateFileSummaries(
const std::vector<manta::FileData>& files_with_summary) {
// Adds all new entries. Overrides any current entries.
for (const manta::FileData& file : files_with_summary) {
// All files added to the index must include a file name, path and summary.
if (file.path.empty() || file.summary.empty()) {
continue;
}
file_summaries_.insert_or_assign(file.path, file);
}
}
std::vector<manta::FileData> SparkyDelegateImpl::GetFileSummaries() {
std::vector<manta::FileData> files_data;
for (const auto& [path, file] : file_summaries_) {
files_data.emplace_back(file);
}
return files_data;
}
} // namespace ash