// 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 "android_webview/browser/aw_browser_context_store.h"
#include <map>
#include <memory>
#include <string>
#include <utility>
#include "android_webview/browser/aw_browser_context.h"
#include "android_webview/browser/aw_browser_process.h"
#include "android_webview/common/aw_features.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/no_destructor.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "android_webview/browser_jni_headers/AwBrowserContextStore_jni.h"
namespace android_webview {
namespace {
constexpr char kProfileNameKey[] = "name";
constexpr char kProfilePathKey[] = "path";
bool g_initialized = false;
const base::FeatureParam<bool> kCreateSpareRendererForDefaultIfMultiProfile{
&features::kCreateSpareRendererOnBrowserContextCreation,
"create_spare_renderer_for_default_if_multi_profile", true};
} // namespace
AwBrowserContextStore::AwBrowserContextStore(PrefService* pref_service)
: prefs_(*pref_service) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
TRACE_EVENT0("startup", "AwBrowserContextStore::AwBrowserContextStore");
ScopedListPrefUpdate update(&*prefs_, prefs::kProfileListPref);
base::Value::List& profiles = update.Get();
for (const auto& profile : profiles) {
const base::Value::Dict& profile_dict = profile.GetDict();
const std::string* name = profile_dict.FindString(kProfileNameKey);
CHECK(name);
const std::string* path_string = profile_dict.FindString(kProfilePathKey);
CHECK(path_string);
CHECK(!path_string->empty());
CHECK_NE(*path_string, base::FilePath::kCurrentDirectory);
CHECK_NE(*path_string, base::FilePath::kParentDirectory);
CHECK_EQ(path_string->find_first_of(base::FilePath::kSeparators),
std::string::npos);
base::FilePath path = base::FilePath(*path_string);
const bool name_is_new =
contexts_.emplace(std::string(*name), Entry(std::move(path), nullptr))
.second;
CHECK(name_is_new);
}
// Ensure default profile entry exists (in both prefs and our data structure)
// and initialize it.
default_context_ = Get(kDefaultContextName, true);
}
bool AwBrowserContextStore::Exists(const std::string& name) const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return contexts_.find(name) != contexts_.end();
}
std::vector<std::string> AwBrowserContextStore::List() const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::vector<std::string> list;
list.reserve(contexts_.size());
for (const auto& kv : contexts_) {
list.push_back(kv.first);
}
return list;
}
AwBrowserContext* AwBrowserContextStore::Get(const std::string& name,
const bool create_if_needed) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto context_it = contexts_.find(name);
Entry* entry;
if (context_it != contexts_.end()) {
entry = &context_it->second;
} else {
if (create_if_needed) {
entry = CreateNewContext(name);
} else {
return nullptr;
}
}
if (!entry->instance) {
const bool is_default = name == kDefaultContextName;
entry->instance =
std::make_unique<AwBrowserContext>(name, entry->path, is_default);
// Ensure this code path is only taken if the IO thread is already running,
// as it's needed for launching processes.
if (base::FeatureList::IsEnabled(
features::kCreateSpareRendererOnBrowserContextCreation) &&
content::BrowserThread::IsThreadInitialized(
content::BrowserThread::IO) &&
(!is_default || kCreateSpareRendererForDefaultIfMultiProfile.Get())) {
content::RenderProcessHost::WarmupSpareRenderProcessHost(
entry->instance.get());
}
}
return entry->instance.get();
}
AwBrowserContextStore::DeletionResult AwBrowserContextStore::Delete(
const std::string& name) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const auto context_it = contexts_.find(name);
if (context_it == contexts_.end()) {
return DeletionResult::kDoesNotExist;
}
Entry* entry = &context_it->second;
if (entry->instance) {
return DeletionResult::kInUse;
}
ScopedListPrefUpdate update(&*prefs_, prefs::kProfileListPref);
base::Value::List& profiles = update.Get();
for (auto profile_it = profiles.begin(); profile_it != profiles.end();
profile_it++) {
const base::Value::Dict& dict = profile_it->GetDict();
const std::string* cur_name = dict.FindString(kProfileNameKey);
CHECK(cur_name);
if (*cur_name == name) {
const std::string* cur_path = dict.FindString(kProfilePathKey);
CHECK(cur_path);
CHECK_EQ(*cur_path, entry->path.value());
// TODO(crbug.com/40268809): Make this async and backgroundable.
AwBrowserContext::DeleteContext(entry->path);
profiles.erase(profile_it);
contexts_.erase(context_it);
return DeletionResult::kDeleted;
}
}
NOTREACHED() << "Profile exists in memory but not in prefs";
}
base::FilePath AwBrowserContextStore::GetRelativePathForTesting(
const std::string& name) const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const auto context_it = contexts_.find(name);
CHECK(context_it != contexts_.end());
return context_it->second.path;
}
AwBrowserContextStore::Entry* AwBrowserContextStore::CreateNewContext(
const std::string_view name) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto emplace_result = contexts_.emplace(std::string(name), Entry());
// Check it was new
CHECK(emplace_result.second);
Entry* entry = &emplace_result.first->second;
ScopedListPrefUpdate update(&*prefs_, prefs::kProfileListPref);
base::Value::List& profiles = update.Get();
if (name == kDefaultContextName) {
entry->path = base::FilePath(AwBrowserContextStore::kDefaultContextPath);
// Do not store the default profile in prefs - it is implicit.
} else {
int number = AssignNewProfileNumber();
entry->path = base::FilePath(
base::StrCat({"Profile ", base::NumberToString(number)}));
AwBrowserContext::PrepareNewContext(entry->path);
base::Value::Dict profileDict =
base::Value::Dict()
.Set(kProfileNameKey, name)
.Set(kProfilePathKey, entry->path.value());
profiles.Append(std::move(profileDict));
}
return entry;
}
int AwBrowserContextStore::AssignNewProfileNumber() {
int number = prefs_->GetInteger(prefs::kProfileCounterPref) + 1;
CHECK_GE(number, 1);
prefs_->SetInteger(prefs::kProfileCounterPref, number);
return number;
}
AwBrowserContext* AwBrowserContextStore::GetDefault() const {
return default_context_;
}
jboolean JNI_AwBrowserContextStore_CheckNamedContextExists(
JNIEnv* const env,
const base::android::JavaParamRef<jstring>& jname) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return AwBrowserContextStore::GetInstance()->Exists(
base::android::ConvertJavaStringToUTF8(env, jname));
}
base::android::ScopedJavaLocalRef<jobject>
JNI_AwBrowserContextStore_GetNamedContextJava(
JNIEnv* const env,
const base::android::JavaParamRef<jstring>& jname,
jboolean create_if_needed) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
AwBrowserContext* context = AwBrowserContextStore::GetInstance()->Get(
base::android::ConvertJavaStringToUTF8(env, jname), create_if_needed);
return context ? context->GetJavaBrowserContext() : nullptr;
}
jboolean JNI_AwBrowserContextStore_DeleteNamedContext(
JNIEnv* const env,
const base::android::JavaParamRef<jstring>& jname) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const std::string name = base::android::ConvertJavaStringToUTF8(env, jname);
AwBrowserContextStore::DeletionResult result =
AwBrowserContextStore::GetInstance()->Delete(name);
switch (result) {
case AwBrowserContextStore::DeletionResult::kDeleted:
return true;
case AwBrowserContextStore::DeletionResult::kDoesNotExist:
return false;
case AwBrowserContextStore::DeletionResult::kInUse:
const std::string error_message =
base::StrCat({"Cannot delete in-use profile ", name});
env->ThrowNew(env->FindClass("java/lang/IllegalStateException"),
error_message.c_str());
return false;
}
}
base::android::ScopedJavaLocalRef<jstring>
JNI_AwBrowserContextStore_GetNamedContextPathForTesting(
JNIEnv* const env,
const base::android::JavaParamRef<jstring>& jname) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::string name = base::android::ConvertJavaStringToUTF8(env, jname);
AwBrowserContextStore* store = AwBrowserContextStore::GetInstance();
if (!store->Exists(name)) {
return nullptr;
}
base::FilePath path = store->GetRelativePathForTesting(name);
return base::android::ConvertUTF8ToJavaString(env, path.value());
}
base::android::ScopedJavaLocalRef<jobjectArray>
JNI_AwBrowserContextStore_ListAllContexts(JNIEnv* env) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const std::vector<std::string> names =
AwBrowserContextStore::GetInstance()->List();
return base::android::ToJavaArrayOfStrings(env, names);
}
// static
void AwBrowserContextStore::RegisterPrefs(PrefRegistrySimple* registry) {
registry->RegisterListPref(prefs::kProfileListPref, base::Value::List());
registry->RegisterIntegerPref(prefs::kProfileCounterPref, 0);
}
// static
AwBrowserContextStore* AwBrowserContextStore::GetInstance() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
CHECK(g_initialized);
return GetOrCreateInstance();
}
// static
AwBrowserContextStore* AwBrowserContextStore::GetOrCreateInstance() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
static base::NoDestructor<AwBrowserContextStore> instance(
AwBrowserProcess::GetInstance()->local_state());
g_initialized = true;
return instance.get();
}
AwBrowserContextStore::Entry::Entry() = default;
AwBrowserContextStore::Entry::Entry(
base::FilePath&& path,
std::unique_ptr<AwBrowserContext>&& instance)
: path(std::move(path)), instance(std::move(instance)) {}
AwBrowserContextStore::Entry::Entry(Entry&&) = default;
AwBrowserContextStore::Entry::~Entry() = default;
} // namespace android_webview