blob: f8f209deb7e6df0ae144b9d6c8ad435c34f55c98 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/web_applications/externally_installed_web_app_prefs.h"
#include <string>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/browser/web_applications/externally_installed_prefs_migration_metrics.h"
#include "chrome/browser/web_applications/user_uninstalled_preinstalled_web_app_prefs.h"
#include "chrome/browser/web_applications/web_app_install_utils.h"
#include "chrome/browser/web_applications/web_app_prefs_utils.h"
#include "chrome/browser/web_applications/web_app_registry_update.h"
#include "chrome/common/pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/browser_thread.h"
#include "url/gurl.h"
namespace web_app {
namespace {
// The stored preferences look like:
//
// "web_apps": {
// "extension_ids": {
// "https://events.google.com/io2016/?utm_source=web_app_manifest": {
// "extension_id": "mjgafbdfajpigcjmkgmeokfbodbcfijl",
// "install_source": 1,
// "is_placeholder": true,
// },
// "https://www.chromestatus.com/features": {
// "extension_id": "fedbieoalmbobgfjapopkghdmhgncnaa",
// "install_source": 1
// }
// }
// }
//
// From the top, prefs::kWebAppsExtensionIDs is "web_apps.extension_ids".
//
// Two levels in is a dictionary (key/value pairs) whose keys are URLs and
// values are leaf dictionaries. Those leaf dictionaries have keys such as
// kExtensionId and kInstallSource.
// The name "extension_id" comes from when PWAs were only backed by the
// Extension system rather than their own. It cannot be changed now that it
// lives persistently in users' profiles.
constexpr char kExtensionId[] = "extension_id";
constexpr char kInstallSource[] = "install_source";
constexpr char kIsPlaceholder[] = "is_placeholder";
// Returns the base::Value in |pref_service| corresponding to our stored dict
// for |app_id|, or nullptr if it doesn't exist.
const base::Value* GetPreferenceValue(const PrefService* pref_service,
const AppId& app_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const base::Value::Dict& urls_to_dicts =
pref_service->GetValueDict(prefs::kWebAppsExtensionIDs);
// Do a simple O(N) scan for app_id being a value in each dictionary's
// key/value pairs. We expect both N and the number of times
// GetPreferenceValue is called to be relatively small in practice. If they
// turn out to be large, we can write a more sophisticated implementation.
for (auto it : urls_to_dicts) {
const base::Value* root = &it.second;
const base::Value* v = root;
if (v->is_dict()) {
v = v->FindKey(kExtensionId);
if (v && v->is_string() && (v->GetString() == app_id)) {
return root;
}
}
}
return nullptr;
}
} // namespace
// static
void ExternallyInstalledWebAppPrefs::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterDictionaryPref(prefs::kWebAppsExtensionIDs);
}
// static
bool ExternallyInstalledWebAppPrefs::HasAppId(const PrefService* pref_service,
const AppId& app_id) {
return GetPreferenceValue(pref_service, app_id) != nullptr;
}
// static
// TODO(crbug.com/1236159): Can be removed after M99.
void ExternallyInstalledWebAppPrefs::RemoveTerminalPWA(
PrefService* pref_service) {
DictionaryPrefUpdate update(pref_service, prefs::kWebAppsExtensionIDs);
update->RemoveKey("chrome-untrusted://terminal/html/pwa.html");
}
// static
bool ExternallyInstalledWebAppPrefs::HasAppIdWithInstallSource(
const PrefService* pref_service,
const AppId& app_id,
ExternalInstallSource install_source) {
const base::Value* v = GetPreferenceValue(pref_service, app_id);
if (v == nullptr || !v->is_dict())
return false;
v = v->FindKeyOfType(kInstallSource, base::Value::Type::INTEGER);
return (v && v->GetInt() == static_cast<int>(install_source));
}
// static
base::flat_map<AppId, base::flat_set<GURL>>
ExternallyInstalledWebAppPrefs::BuildAppIdsMap(
const PrefService* pref_service,
ExternalInstallSource install_source) {
const base::Value::Dict& urls_to_dicts =
pref_service->GetValueDict(prefs::kWebAppsExtensionIDs);
base::flat_map<AppId, base::flat_set<GURL>> ids_to_urls;
for (auto it : urls_to_dicts) {
const base::Value* v = &it.second;
if (!v->is_dict()) {
continue;
}
const base::Value* install_source_value =
v->FindKeyOfType(kInstallSource, base::Value::Type::INTEGER);
if (!install_source_value ||
(install_source_value->GetInt() != static_cast<int>(install_source))) {
continue;
}
v = v->FindKey(kExtensionId);
if (!v || !v->is_string()) {
continue;
}
GURL url(it.first);
DCHECK(url.is_valid() && !url.is_empty());
ids_to_urls[v->GetString()] = {url};
}
return ids_to_urls;
}
ExternallyInstalledWebAppPrefs::ExternallyInstalledWebAppPrefs(
PrefService* pref_service)
: pref_service_(pref_service) {}
void ExternallyInstalledWebAppPrefs::Insert(
const GURL& url,
const AppId& app_id,
ExternalInstallSource install_source) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::Value dict(base::Value::Type::DICTIONARY);
dict.SetKey(kExtensionId, base::Value(app_id));
dict.SetKey(kInstallSource, base::Value(static_cast<int>(install_source)));
DictionaryPrefUpdate update(pref_service_, prefs::kWebAppsExtensionIDs);
update->SetKey(url.spec(), std::move(dict));
}
bool ExternallyInstalledWebAppPrefs::Remove(const GURL& url) {
DictionaryPrefUpdate update(pref_service_, prefs::kWebAppsExtensionIDs);
return update->RemoveKey(url.spec());
}
absl::optional<AppId> ExternallyInstalledWebAppPrefs::LookupAppId(
const GURL& url) const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const base::Value* v =
pref_service_->GetDictionary(prefs::kWebAppsExtensionIDs)
->FindKey(url.spec());
if (v && v->is_dict()) {
v = v->FindKey(kExtensionId);
if (v && v->is_string()) {
return absl::make_optional(v->GetString());
}
}
return absl::nullopt;
}
absl::optional<AppId> ExternallyInstalledWebAppPrefs::LookupPlaceholderAppId(
const GURL& url) const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const base::Value* entry =
pref_service_->GetDictionary(prefs::kWebAppsExtensionIDs)
->FindKey(url.spec());
if (!entry)
return absl::nullopt;
absl::optional<bool> is_placeholder = entry->FindBoolKey(kIsPlaceholder);
if (!is_placeholder.has_value() || !is_placeholder.value())
return absl::nullopt;
return *entry->FindStringKey(kExtensionId);
}
void ExternallyInstalledWebAppPrefs::SetIsPlaceholder(const GURL& url,
bool is_placeholder) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(pref_service_->GetDictionary(prefs::kWebAppsExtensionIDs)
->FindKey(url.spec()));
DictionaryPrefUpdate update(pref_service_, prefs::kWebAppsExtensionIDs);
base::Value* map = update.Get();
auto* app_entry = map->FindKey(url.spec());
DCHECK(app_entry);
app_entry->SetBoolKey(kIsPlaceholder, is_placeholder);
}
bool ExternallyInstalledWebAppPrefs::IsPlaceholderApp(
const AppId& app_id) const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const base::Value* app_prefs = GetPreferenceValue(pref_service_, app_id);
if (!app_prefs || !app_prefs->is_dict())
return false;
return app_prefs->FindBoolKey(kIsPlaceholder).value_or(false);
}
// static
ExternallyInstalledWebAppPrefs::ParsedPrefs
ExternallyInstalledWebAppPrefs::ParseExternalPrefsToWebAppData(
PrefService* pref_service) {
const base::Value::Dict& urls_to_dicts =
pref_service->GetValueDict(prefs::kWebAppsExtensionIDs);
ParsedPrefs ids_to_parsed_data;
for (auto it : urls_to_dicts) {
const base::Value* v = &it.second;
if (!v->is_dict()) {
continue;
}
auto* app_id = v->FindKey(kExtensionId);
if (!app_id || !app_id->is_string()) {
continue;
}
auto* source = v->FindKey(kInstallSource);
if (!source) {
continue;
}
WebAppManagement::Type source_type = ConvertExternalInstallSourceToSource(
static_cast<ExternalInstallSource>(source->GetInt()));
WebApp::ExternalManagementConfig& config =
ids_to_parsed_data[app_id->GetString()][source_type];
config.is_placeholder = v->FindBoolKey(kIsPlaceholder).value_or(false);
config.install_urls.emplace(GURL(it.first));
}
return ids_to_parsed_data;
}
// static
void ExternallyInstalledWebAppPrefs::MigrateExternalPrefData(
PrefService* pref_service,
WebAppSyncBridge* sync_bridge) {
ExternallyInstalledWebAppPrefs::ParsedPrefs pref_to_app_data =
ParseExternalPrefsToWebAppData(pref_service);
const WebAppRegistrar& registrar = sync_bridge->registrar();
LogDataMetrics(pref_to_app_data.size() != 0,
registrar.AppsExistWithExternalConfigData());
// First migrate data to UserUninstalledPreinstalledWebAppPrefs.
MigrateExternalPrefDataToPreinstalledPrefs(pref_service, &registrar,
pref_to_app_data);
ScopedRegistryUpdate update(sync_bridge);
for (auto it : pref_to_app_data) {
const WebApp* web_app = registrar.GetAppById(it.first);
if (web_app) {
// Sync data across externally installed prefs and web_app DB.
for (auto parsed_info : it.second) {
WebAppManagement::Type& source = parsed_info.first;
if (!web_app->GetSources().test(source))
continue;
const WebApp::ExternalConfigMap& config_map =
web_app->management_to_external_config_map();
auto map_it = config_map.find(source);
// Placeholder migration and metrics logging.
if (map_it != config_map.end() &&
map_it->second.is_placeholder ==
parsed_info.second.is_placeholder) {
LogPlaceholderMigrationState(
PlaceholderMigrationState::kPlaceholderInfoAlreadyInSync);
} else {
WebApp* updated_app = update->UpdateApp(it.first);
updated_app->AddPlaceholderInfoToManagementExternalConfigMap(
source, parsed_info.second.is_placeholder);
LogPlaceholderMigrationState(
PlaceholderMigrationState::kPlaceholderInfoMigrated);
}
// Install URL migration and metrics logging.
for (auto url : parsed_info.second.install_urls) {
DCHECK(url.is_valid());
if (map_it != config_map.end() &&
base::Contains(map_it->second.install_urls, url)) {
LogInstallURLMigrationState(
InstallURLMigrationState::kInstallURLAlreadyInSync);
} else {
WebApp* updated_app = update->UpdateApp(it.first);
updated_app->AddInstallURLToManagementExternalConfigMap(
parsed_info.first, url);
LogInstallURLMigrationState(
InstallURLMigrationState::kInstallURLMigrated);
}
}
}
}
}
}
// static
void ExternallyInstalledWebAppPrefs::MigrateExternalPrefDataToPreinstalledPrefs(
PrefService* pref_service,
const WebAppRegistrar* registrar,
const ExternallyInstalledWebAppPrefs::ParsedPrefs& parsed_data) {
UserUninstalledPreinstalledWebAppPrefs preinstalled_prefs(pref_service);
for (auto pair : parsed_data) {
const AppId& app_id = pair.first;
const WebApp::ExternalConfigMap& source_to_config_map = pair.second;
const auto& it = source_to_config_map.find(WebAppManagement::kDefault);
// Migration will happen in the following cases:
// 1. If app_id exists in the external prefs that had source as
// kDefault but the app is no longer installed in the registry or if it
// is no longer preinstalled, that means
// it was preinstalled and then uninstalled by user.
if (!registrar->IsInstalledByDefaultManagement(app_id) &&
it != source_to_config_map.end()) {
if (preinstalled_prefs.AppIdContainsAllUrls(app_id, source_to_config_map,
/*only_default=*/true)) {
LogUserUninstalledPreinstalledAppMigration(
UserUninstalledPreinstalledAppMigrationState::
kPreinstalledAppDataAlreadyInSync);
} else {
preinstalled_prefs.Add(app_id, std::move(it->second.install_urls));
LogUserUninstalledPreinstalledAppMigration(
UserUninstalledPreinstalledAppMigrationState::
kPreinstalledAppDataMigratedByUser);
}
}
// 2. If the value corresponding to the app_id in
// kWasExternalAppUninstalledByUser is true, then it was previously
// a preinstalled app that was user uninstalled. In this case, we migrate
// ALL install URLs for that corresponding app_id, because a preinstalled
// app could have been installed as an app with a different source now.
if (GetBoolWebAppPref(pref_service, app_id,
kWasExternalAppUninstalledByUser)) {
if (preinstalled_prefs.AppIdContainsAllUrls(app_id, source_to_config_map,
/*only_default=*/false)) {
LogUserUninstalledPreinstalledAppMigration(
UserUninstalledPreinstalledAppMigrationState::
kPreinstalledAppDataAlreadyInSync);
} else {
base::flat_set<GURL> urls_to_migrate =
MergeAllUrls(source_to_config_map);
preinstalled_prefs.Add(app_id, std::move(urls_to_migrate));
LogUserUninstalledPreinstalledAppMigration(
UserUninstalledPreinstalledAppMigrationState::
kPreinstalledAppDataMigratedByOldPref);
}
}
}
}
// static
base::flat_set<GURL> ExternallyInstalledWebAppPrefs::MergeAllUrls(
const WebApp::ExternalConfigMap& source_config_map) {
std::vector<GURL> urls;
for (auto it : source_config_map) {
for (const GURL& url : it.second.install_urls) {
DCHECK(url.is_valid());
urls.push_back(url);
}
}
return urls;
}
// static
void ExternallyInstalledWebAppPrefs::LogDataMetrics(
bool data_exists_in_pref,
bool data_exists_in_registrar) {
// Data in registry refers to the data stored in
// management_to_external_config_map per web_app. See
// WebApps::management_to_external_config_map() for more info.
if (!data_exists_in_pref && !data_exists_in_registrar) {
// Case 1: No external apps installed (prefs empty, empty data per web_app
// in the registry).
base::UmaHistogramBoolean(kPrefDataAbsentDBDataAbsent, /*sample=*/true);
} else if (!data_exists_in_pref && data_exists_in_registrar) {
// Case 2: prefs are empty, but data exists in registry.
base::UmaHistogramBoolean(kPrefDataAbsentDBDataPresent, /*sample=*/true);
} else if (data_exists_in_pref && !data_exists_in_registrar) {
// Case 3: prefs contain data, but data does not exist in the registry.
base::UmaHistogramBoolean(kPrefDataPresentDBDataAbsent, /*sample=*/true);
} else {
// Case 4: Data exists in both prefs and in the registry.
base::UmaHistogramBoolean(kPrefDataPresentDBDataPresent, /*sample=*/true);
}
}
} // namespace web_app