| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/services/app_service/public/cpp/preferred_apps_impl.h" |
| |
| #include <iterator> |
| #include <utility> |
| |
| #include "base/containers/contains.h" |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/json/json_string_value_serializer.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/scoped_blocking_call.h" |
| #include "components/services/app_service/public/cpp/intent_filter_util.h" |
| #include "components/services/app_service/public/cpp/preferred_apps_converter.h" |
| #include "content/public/browser/browser_thread.h" |
| |
| namespace { |
| |
| const base::FilePath::CharType kPreferredAppsDirname[] = |
| FILE_PATH_LITERAL("PreferredApps"); |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| enum class PreferredAppsFileIOAction { |
| kWriteSuccess = 0, |
| kWriteFailed = 1, |
| kReadSuccess = 2, |
| kReadFailed = 3, |
| kMaxValue = kReadFailed, |
| }; |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| enum class PreferredAppsUpdateAction { |
| kAdd = 0, |
| kDeleteForFilter = 1, |
| kDeleteForAppId = 2, |
| kUpgraded = 3, |
| kMaxValue = kUpgraded, |
| }; |
| |
| void LogPreferredAppEntryCount(int entry_count) { |
| base::UmaHistogramCounts10000("Apps.PreferredApps.EntryCount", entry_count); |
| } |
| |
| // Performs blocking I/O. Called on another thread. |
| void WriteDataBlocking(const base::FilePath& preferred_apps_file, |
| const std::string& preferred_apps) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| bool write_success = |
| base::WriteFile(preferred_apps_file, preferred_apps.c_str(), |
| preferred_apps.size()) != -1; |
| if (!write_success) { |
| DVLOG(0) << "Fail to write preferred apps to " << preferred_apps_file; |
| } |
| } |
| |
| // Performs blocking I/O. Called on another thread. |
| std::string ReadDataBlocking(const base::FilePath& preferred_apps_file) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| std::string preferred_apps_string; |
| base::ReadFileToString(preferred_apps_file, &preferred_apps_string); |
| return preferred_apps_string; |
| } |
| |
| } // namespace |
| |
| namespace apps { |
| |
| PreferredAppsImpl::PreferredAppsImpl( |
| Host* host, |
| const base::FilePath& profile_dir, |
| base::OnceClosure read_completed_for_testing, |
| base::OnceClosure write_completed_for_testing) |
| : host_(host), |
| profile_dir_(profile_dir), |
| task_runner_(base::ThreadPool::CreateSequencedTaskRunner( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})), |
| read_completed_for_testing_(std::move(read_completed_for_testing)), |
| write_completed_for_testing_(std::move(write_completed_for_testing)) { |
| DCHECK(host_); |
| InitializePreferredApps(); |
| } |
| |
| PreferredAppsImpl::~PreferredAppsImpl() = default; |
| |
| void PreferredAppsImpl::AddPreferredApp(AppType app_type, |
| const std::string& app_id, |
| IntentFilterPtr intent_filter, |
| IntentPtr intent, |
| bool from_publisher) { |
| RunAfterPreferredAppsReady(base::BindOnce( |
| &PreferredAppsImpl::AddPreferredAppImpl, weak_ptr_factory_.GetWeakPtr(), |
| app_type, app_id, std::move(intent_filter), std::move(intent), |
| from_publisher)); |
| } |
| |
| void PreferredAppsImpl::RemovePreferredApp(const std::string& app_id) { |
| RunAfterPreferredAppsReady( |
| base::BindOnce(&PreferredAppsImpl::RemovePreferredAppImpl, |
| weak_ptr_factory_.GetWeakPtr(), app_id)); |
| } |
| |
| void PreferredAppsImpl::SetSupportedLinksPreference( |
| AppType app_type, |
| const std::string& app_id, |
| IntentFilters all_link_filters) { |
| RunAfterPreferredAppsReady( |
| base::BindOnce(&PreferredAppsImpl::SetSupportedLinksPreferenceImpl, |
| weak_ptr_factory_.GetWeakPtr(), app_type, app_id, |
| std::move(all_link_filters))); |
| } |
| |
| void PreferredAppsImpl::RemoveSupportedLinksPreference( |
| AppType app_type, |
| const std::string& app_id) { |
| RunAfterPreferredAppsReady( |
| base::BindOnce(&PreferredAppsImpl::RemoveSupportedLinksPreferenceImpl, |
| weak_ptr_factory_.GetWeakPtr(), app_type, app_id)); |
| } |
| |
| void PreferredAppsImpl::InitializePreferredApps() { |
| ReadFromJSON(profile_dir_); |
| } |
| |
| void PreferredAppsImpl::WriteToJSON( |
| const base::FilePath& profile_dir, |
| const apps::PreferredAppsList& preferred_apps) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| // If currently is writing preferred apps to file, set a flag to write after |
| // the current write completed. |
| if (writing_preferred_apps_) { |
| should_write_preferred_apps_to_file_ = true; |
| return; |
| } |
| |
| writing_preferred_apps_ = true; |
| |
| auto preferred_apps_value = |
| apps::ConvertPreferredAppsToValue(preferred_apps.GetReference()); |
| |
| std::string json_string; |
| JSONStringValueSerializer serializer(&json_string); |
| serializer.Serialize(preferred_apps_value); |
| |
| task_runner_->PostTaskAndReply( |
| FROM_HERE, |
| base::BindOnce(&WriteDataBlocking, |
| profile_dir.Append(kPreferredAppsDirname), json_string), |
| base::BindOnce(&PreferredAppsImpl::WriteCompleted, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void PreferredAppsImpl::WriteCompleted() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| writing_preferred_apps_ = false; |
| if (!should_write_preferred_apps_to_file_) { |
| // Call the testing callback if it is set. |
| if (write_completed_for_testing_) { |
| std::move(write_completed_for_testing_).Run(); |
| } |
| return; |
| } |
| // If need to perform another write, write the most up to date preferred apps |
| // from memory to file. |
| should_write_preferred_apps_to_file_ = false; |
| WriteToJSON(profile_dir_, preferred_apps_list_); |
| } |
| |
| void PreferredAppsImpl::ReadFromJSON(const base::FilePath& profile_dir) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&ReadDataBlocking, |
| profile_dir.Append(kPreferredAppsDirname)), |
| base::BindOnce(&PreferredAppsImpl::ReadCompleted, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void PreferredAppsImpl::ReadCompleted(std::string preferred_apps_string) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| bool preferred_apps_upgraded = false; |
| if (preferred_apps_string.empty()) { |
| preferred_apps_list_.Init(); |
| } else { |
| std::string json_string; |
| JSONStringValueDeserializer deserializer(preferred_apps_string); |
| int error_code; |
| std::string error_message; |
| auto preferred_apps_value = |
| deserializer.Deserialize(&error_code, &error_message); |
| |
| if (!preferred_apps_value) { |
| DVLOG(0) << "Fail to deserialize json value from string with error code: " |
| << error_code << " and error message: " << error_message; |
| preferred_apps_list_.Init(); |
| } else { |
| preferred_apps_upgraded = IsUpgradedForSharing(*preferred_apps_value); |
| auto preferred_apps = |
| apps::ParseValueToPreferredApps(*preferred_apps_value); |
| if (!preferred_apps_upgraded) { |
| UpgradePreferredApps(preferred_apps); |
| } |
| preferred_apps_list_.Init(std::move(preferred_apps)); |
| } |
| } |
| if (!preferred_apps_upgraded) { |
| WriteToJSON(profile_dir_, preferred_apps_list_); |
| } |
| |
| host_->InitializePreferredAppsForAllSubscribers(); |
| |
| LogPreferredAppEntryCount(preferred_apps_list_.GetEntrySize()); |
| |
| while (!pending_preferred_apps_tasks_.empty()) { |
| std::move(pending_preferred_apps_tasks_.front()).Run(); |
| pending_preferred_apps_tasks_.pop(); |
| } |
| |
| if (read_completed_for_testing_) { |
| std::move(read_completed_for_testing_).Run(); |
| } |
| } |
| |
| void PreferredAppsImpl::RunAfterPreferredAppsReady(base::OnceClosure task) { |
| if (preferred_apps_list_.IsInitialized()) { |
| std::move(task).Run(); |
| } else { |
| pending_preferred_apps_tasks_.push(std::move(task)); |
| } |
| } |
| |
| void PreferredAppsImpl::AddPreferredAppImpl(AppType app_type, |
| const std::string& app_id, |
| IntentFilterPtr intent_filter, |
| IntentPtr intent, |
| bool from_publisher) { |
| DCHECK(!app_id.empty()); |
| |
| auto replaced_apps = |
| preferred_apps_list_.AddPreferredApp(app_id, intent_filter); |
| |
| WriteToJSON(profile_dir_, preferred_apps_list_); |
| |
| auto changes = std::make_unique<PreferredAppChanges>(); |
| changes->added_filters[app_id].push_back(intent_filter->Clone()); |
| changes->removed_filters = CloneIntentFiltersMap(replaced_apps); |
| host_->OnPreferredAppsChanged(std::move(changes)); |
| |
| if (from_publisher || !intent) { |
| return; |
| } |
| |
| // Sync the change to publishers. Because |replaced_app_preference| can |
| // be any app type, we should run this for all publishers. Currently |
| // only implemented in ARC publisher. |
| // TODO(crbug.com/1322000): The |replaced_app_preference| can be really big, |
| // update this logic to only call the relevant publisher for each app after |
| // updating the storage structure. |
| host_->OnPreferredAppSet(app_id, std::move(intent_filter), std::move(intent), |
| std::move(replaced_apps)); |
| } |
| |
| void PreferredAppsImpl::RemovePreferredAppImpl(const std::string& app_id) { |
| IntentFilters removed_filters = preferred_apps_list_.DeleteAppId(app_id); |
| if (!removed_filters.empty()) { |
| WriteToJSON(profile_dir_, preferred_apps_list_); |
| |
| auto changes = std::make_unique<PreferredAppChanges>(); |
| changes->removed_filters[app_id] = std::move(removed_filters); |
| host_->OnPreferredAppsChanged(std::move(changes)); |
| } |
| } |
| |
| void PreferredAppsImpl::SetSupportedLinksPreferenceImpl( |
| AppType app_type, |
| const std::string& app_id, |
| IntentFilters all_link_filters) { |
| auto changes = std::make_unique<PreferredAppChanges>(); |
| auto& added = changes->added_filters; |
| auto& removed = changes->removed_filters; |
| |
| for (auto& filter : all_link_filters) { |
| auto replaced_apps = preferred_apps_list_.AddPreferredApp(app_id, filter); |
| added[app_id].push_back(std::move(filter)); |
| |
| // If we removed overlapping supported links when adding the new app, those |
| // affected apps no longer handle all their Supported Links filters and so |
| // need to have all their other Supported Links filters removed. |
| // Additionally, track all removals in the |removed| map so that subscribers |
| // can be notified correctly. |
| for (auto& replaced_app_and_filters : replaced_apps) { |
| const std::string& removed_app_id = replaced_app_and_filters.first; |
| bool first_removal_for_app = !base::Contains(removed, app_id); |
| bool did_replace_supported_link = base::ranges::any_of( |
| replaced_app_and_filters.second, |
| [&removed_app_id](const auto& filter) { |
| return apps_util::IsSupportedLinkForApp(removed_app_id, filter); |
| }); |
| |
| IntentFilters& removed_filters_for_app = removed[removed_app_id]; |
| removed_filters_for_app.insert( |
| removed_filters_for_app.end(), |
| std::make_move_iterator(replaced_app_and_filters.second.begin()), |
| std::make_move_iterator(replaced_app_and_filters.second.end())); |
| |
| // We only need to remove other supported links once per app. |
| if (first_removal_for_app && did_replace_supported_link) { |
| IntentFilters removed_filters = |
| preferred_apps_list_.DeleteSupportedLinks(removed_app_id); |
| removed_filters_for_app.insert( |
| removed_filters_for_app.end(), |
| std::make_move_iterator(removed_filters.begin()), |
| std::make_move_iterator(removed_filters.end())); |
| } |
| } |
| } |
| |
| WriteToJSON(profile_dir_, preferred_apps_list_); |
| |
| host_->OnPreferredAppsChanged(changes->Clone()); |
| |
| // Notify publishers: The new app has been set to open links, and all removed |
| // apps no longer handle links. |
| if (host_->HasPublisher(app_type)) { |
| host_->OnSupportedLinksPreferenceChanged(app_type, app_id, |
| /*open_in_app=*/true); |
| } |
| for (const auto& removed_app_and_filters : removed) { |
| // We don't know what app type the app is, so we have to notify all |
| // publishers. |
| // TODO(crbug.com/1322000): Only notify the relevant publishers. |
| host_->OnSupportedLinksPreferenceChanged(removed_app_and_filters.first, |
| /*open_in_app=*/false); |
| } |
| } |
| void PreferredAppsImpl::RemoveSupportedLinksPreferenceImpl( |
| AppType app_type, |
| const std::string& app_id) { |
| if (!host_->HasPublisher(app_type)) { |
| return; |
| } |
| |
| IntentFilters removed_filters = |
| preferred_apps_list_.DeleteSupportedLinks(app_id); |
| |
| if (!removed_filters.empty()) { |
| WriteToJSON(profile_dir_, preferred_apps_list_); |
| |
| auto changes = std::make_unique<PreferredAppChanges>(); |
| changes->removed_filters[app_id] = std::move(removed_filters); |
| host_->OnPreferredAppsChanged(std::move(changes)); |
| } |
| |
| host_->OnSupportedLinksPreferenceChanged(app_type, app_id, |
| /*open_in_app=*/false); |
| } |
| |
| } // namespace apps |