// Copyright 2020 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/android/historical_tab_saver.h"
#include <map>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include "base/android/jni_android.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/android/token_android.h"
#include "base/memory/raw_ptr.h"
#include "base/not_fatal_until.h"
#include "base/uuid.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/tab/web_contents_state.h"
#include "chrome/browser/ui/android/tab_model/android_live_tab_context_wrapper.h"
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#include "chrome/common/url_constants.h"
#include "components/sessions/content/content_live_tab.h"
#include "components/sessions/core/tab_restore_service.h"
#include "content/public/browser/web_contents.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/android/chrome_jni_headers/HistoricalTabSaverImpl_jni.h"
using base::android::JavaParamRef;
using base::android::JavaRef;
using base::android::ScopedJavaLocalRef;
namespace historical_tab_saver {
namespace {
// Defined in TabGroupModelFilter.java
constexpr int kInvalidRootId = -1;
std::vector<WebContentsStateByteBuffer> AllTabsWebContentsStateByteBuffer(
JNIEnv* env,
const JavaParamRef<jobjectArray>& jbyte_buffers,
std::vector<int> saved_state_versions) {
int jbyte_buffers_count = env->GetArrayLength(jbyte_buffers);
std::vector<WebContentsStateByteBuffer> web_contents_states;
web_contents_states.reserve(jbyte_buffers_count);
for (int i = 0; i < jbyte_buffers_count; ++i) {
web_contents_states.emplace_back(
ScopedJavaLocalRef<jobject>(
env, env->GetObjectArrayElement(jbyte_buffers, i)),
saved_state_versions[i]);
}
return web_contents_states;
}
std::optional<tab_groups::TabGroupId> JavaTokenToTabGroupId(
JNIEnv* env,
const JavaRef<jobject>& jtab_group_id) {
if (jtab_group_id.is_null()) {
return std::nullopt;
}
return tab_groups::TabGroupId::FromRawToken(
base::android::TokenAndroid::FromJavaToken(env, jtab_group_id));
}
std::vector<std::optional<tab_groups::TabGroupId>> JavaTokensToTabGroupIds(
JNIEnv* env,
const JavaParamRef<jobjectArray>& jtab_group_ids) {
std::vector<std::optional<tab_groups::TabGroupId>> tab_group_ids;
size_t array_length = env->GetArrayLength(jtab_group_ids);
tab_group_ids.reserve(array_length);
for (size_t i = 0; i < array_length; ++i) {
auto jtab_group_id = env->GetObjectArrayElement(jtab_group_ids, i);
std::optional<tab_groups::TabGroupId> tab_group_id = JavaTokenToTabGroupId(
env, ScopedJavaLocalRef<jobject>(env, jtab_group_id));
tab_group_ids.push_back(tab_group_id);
}
return tab_group_ids;
}
std::optional<base::Uuid> StringToUuid(
const std::u16string& serialized_saved_tab_group_id) {
if (serialized_saved_tab_group_id.empty()) {
return std::nullopt;
}
return base::Uuid::ParseLowercase(serialized_saved_tab_group_id);
}
std::vector<std::optional<base::Uuid>> StringsToUuids(
const std::vector<std::u16string>& serialized_saved_tab_group_ids) {
std::vector<std::optional<base::Uuid>> saved_tab_group_ids;
saved_tab_group_ids.reserve(serialized_saved_tab_group_ids.size());
for (const auto& serialized_saved_tab_group_id :
serialized_saved_tab_group_ids) {
saved_tab_group_ids.push_back(StringToUuid(serialized_saved_tab_group_id));
}
return saved_tab_group_ids;
}
void CreateHistoricalTab(
TabAndroid* tab_android,
WebContentsStateByteBuffer web_contents_state_byte_buffer) {
if (!tab_android) {
return;
}
auto scoped_web_contents = ScopedWebContents::CreateForTab(
tab_android, &web_contents_state_byte_buffer);
if (!scoped_web_contents->web_contents()) {
return;
}
sessions::TabRestoreService* service =
TabRestoreServiceFactory::GetForProfile(Profile::FromBrowserContext(
scoped_web_contents->web_contents()->GetBrowserContext()));
if (!service) {
return;
}
// TODO(crbug/41496693): We should update AndroidLiveTabContext to return
// group data for single tabs when not closing an entire group to align with
// desktop. Right now any individual tab closure is treated as not being in a
// group.
// Index is unimportant on Android.
service->CreateHistoricalTab(sessions::ContentLiveTab::GetForWebContents(
scoped_web_contents->web_contents()),
/*index=*/-1);
}
void CreateHistoricalGroup(
TabModel* model,
const std::optional<tab_groups::TabGroupId>& optional_tab_group_id,
const std::optional<base::Uuid> saved_tab_group_id,
const std::u16string& group_title,
int group_color,
std::vector<raw_ptr<TabAndroid, VectorExperimental>> tabs,
std::vector<WebContentsStateByteBuffer> web_contents_state) {
DCHECK(model);
sessions::TabRestoreService* service =
TabRestoreServiceFactory::GetForProfile(model->GetProfile());
if (!service) {
return;
}
tab_groups::TabGroupId group_id = optional_tab_group_id
? *optional_tab_group_id
: tab_groups::TabGroupId::GenerateNew();
std::map<int, tab_groups::TabGroupId> tab_id_to_group_id;
for (const TabAndroid* tab : tabs) {
DCHECK(tab);
tab_id_to_group_id.insert(std::make_pair(tab->GetAndroidId(), group_id));
}
// TODO(crbug/41496693): If we update AndroidLiveTabContext to return group
// data for tabs it should be possible to eliminate the need for this wrapper
// when closing an entire tab group.
AndroidLiveTabContextCloseWrapper context(
model, std::move(tabs), std::move(tab_id_to_group_id),
std::map<tab_groups::TabGroupId, tab_groups::TabGroupVisualData>(
{{group_id, tab_groups::TabGroupVisualData(
group_title, /*color_int=*/(
tab_groups::TabGroupColorId)group_color)}}),
std::map<tab_groups::TabGroupId, std::optional<base::Uuid>>(
{{group_id, saved_tab_group_id}}),
std::move(web_contents_state));
service->CreateHistoricalGroup(&context, group_id);
service->GroupClosed(group_id);
}
void CreateHistoricalBulkClosure(
TabModel* model,
std::vector<int> root_ids,
std::vector<std::optional<tab_groups::TabGroupId>> optional_tab_group_ids,
std::vector<std::optional<base::Uuid>> saved_tab_group_ids,
std::vector<std::u16string> group_titles,
std::vector<int> group_colors,
std::vector<int> per_tab_root_id,
std::vector<raw_ptr<TabAndroid, VectorExperimental>> tabs,
std::vector<WebContentsStateByteBuffer> web_contents_state) {
DCHECK(model);
DCHECK_EQ(root_ids.size(), group_titles.size());
DCHECK_EQ(root_ids.size(), group_colors.size());
DCHECK_EQ(root_ids.size(), optional_tab_group_ids.size());
DCHECK_EQ(root_ids.size(), saved_tab_group_ids.size());
DCHECK_EQ(per_tab_root_id.size(), tabs.size());
sessions::TabRestoreService* service =
TabRestoreServiceFactory::GetForProfile(model->GetProfile());
if (!service) {
return;
}
// Map each Android Group IDs to a stand-in tab_group::TabGroupId for storage
// in TabRestoreService.
std::map<int, tab_groups::TabGroupId> group_id_mapping;
// Map each tab_group::TabGroupId to corresponding data for consumption
// downstream.
std::map<tab_groups::TabGroupId, tab_groups::TabGroupVisualData>
tab_group_visual_data;
std::map<tab_groups::TabGroupId, std::optional<base::Uuid>>
saved_tab_group_ids_map;
for (size_t i = 0; i < root_ids.size(); ++i) {
auto group_id = tab_groups::TabGroupId::CreateEmpty();
auto optional_tab_group_id = optional_tab_group_ids[i];
if (optional_tab_group_id) {
group_id = *optional_tab_group_id;
} else {
group_id = tab_groups::TabGroupId::GenerateNew();
// Avoid collision - highly unlikely for 128 bit int.
while (tab_group_visual_data.count(group_id)) {
group_id = tab_groups::TabGroupId::GenerateNew();
}
}
int root_id = root_ids[i];
group_id_mapping.insert({root_id, group_id});
auto saved_tab_group_id = saved_tab_group_ids[i];
if (saved_tab_group_id) {
saved_tab_group_ids_map.insert({group_id, saved_tab_group_id});
}
const std::u16string title = group_titles[i];
int color = group_colors[i];
tab_group_visual_data[group_id] = tab_groups::TabGroupVisualData(
title, /*color=*/(tab_groups::TabGroupColorId)color);
}
// Map Android Tabs by ID to their new or existing native
// tab_group::TabGroupId.
std::map<int, tab_groups::TabGroupId> tab_id_to_group_id;
for (size_t i = 0; i < tabs.size(); ++i) {
TabAndroid* tab = tabs[i];
if (per_tab_root_id[i] != kInvalidRootId) {
int root_id = per_tab_root_id[i];
auto it = group_id_mapping.find(root_id);
CHECK(it != group_id_mapping.end(), base::NotFatalUntil::M130);
tab_id_to_group_id.insert(
std::make_pair(tab->GetAndroidId(), it->second));
}
}
// This wrapper is necessary for bulk closures that don't close all tabs via
// the bulk tab editor.
AndroidLiveTabContextCloseWrapper context(
model, std::move(tabs), std::move(tab_id_to_group_id),
std::move(tab_group_visual_data), std::move(saved_tab_group_ids_map),
std::move(web_contents_state));
service->BrowserClosing(&context);
service->BrowserClosed(&context);
}
} // namespace
ScopedWebContents::ScopedWebContents(content::WebContents* unowned_web_contents)
: unowned_web_contents_(unowned_web_contents),
owned_web_contents_(nullptr) {}
ScopedWebContents::ScopedWebContents(
std::unique_ptr<content::WebContents> owned_web_contents)
: unowned_web_contents_(nullptr),
owned_web_contents_(std::move(owned_web_contents)) {
if (owned_web_contents_) {
owned_web_contents_->SetOwnerLocationForDebug(FROM_HERE);
}
}
ScopedWebContents::~ScopedWebContents() = default;
content::WebContents* ScopedWebContents::web_contents() const {
if (!unowned_web_contents_) {
return owned_web_contents_.get();
} else {
return unowned_web_contents_;
}
}
// static
std::unique_ptr<ScopedWebContents> ScopedWebContents::CreateForTab(
TabAndroid* tab,
const WebContentsStateByteBuffer* web_contents_state) {
if (tab->web_contents()) {
return std::make_unique<ScopedWebContents>(tab->web_contents());
}
if (web_contents_state->state_version != -1) {
auto native_contents = WebContentsState::RestoreContentsFromByteBuffer(
web_contents_state, /*initially_hidden=*/true, /*no_renderer=*/true);
if (native_contents) {
return std::make_unique<ScopedWebContents>(std::move(native_contents));
}
}
// Fallback to an empty web contents in the event state restoration
// fails. This will just not be added to the TabRestoreService.
// This is only called on non-incognito pathways.
CHECK(!tab->IsIncognito());
Profile* profile = ProfileManager::GetActiveUserProfile();
content::WebContents::CreateParams params(profile);
params.initially_hidden = true;
params.desired_renderer_state =
content::WebContents::CreateParams::kNoRendererProcess;
return std::make_unique<ScopedWebContents>(
content::WebContents::Create(params));
}
// Static JNI methods.
static void JNI_HistoricalTabSaverImpl_CreateHistoricalTab(
JNIEnv* env,
const JavaParamRef<jobject>& jtab_android,
const JavaParamRef<jobject>& state,
jint saved_state_version) {
WebContentsStateByteBuffer web_contents_state = WebContentsStateByteBuffer(
ScopedJavaLocalRef<jobject>(state), (int)saved_state_version);
CreateHistoricalTab(TabAndroid::GetNativeTab(env, jtab_android),
std::move(web_contents_state));
}
static void JNI_HistoricalTabSaverImpl_CreateHistoricalGroup(
JNIEnv* env,
const JavaParamRef<jobject>& jtab_model,
const JavaParamRef<jobject>& jtab_group_id,
std::u16string& serialized_saved_tab_group_id,
std::u16string& title,
jint jcolor,
const JavaParamRef<jobjectArray>& jtabs_android,
const JavaParamRef<jobjectArray>& jbyte_buffers,
std::vector<int32_t>& saved_state_versions) {
std::optional<tab_groups::TabGroupId> tab_group_id =
JavaTokenToTabGroupId(env, jtab_group_id);
std::optional<base::Uuid> saved_tab_group_id =
StringToUuid(serialized_saved_tab_group_id);
auto tabs_android = TabAndroid::GetAllNativeTabs(
env, base::android::ScopedJavaLocalRef<jobjectArray>(jtabs_android));
int tabs_android_count = env->GetArrayLength(jtabs_android);
DCHECK_EQ(tabs_android_count, env->GetArrayLength(jbyte_buffers));
DCHECK_EQ(tabs_android_count, static_cast<int>(saved_state_versions.size()));
std::vector<WebContentsStateByteBuffer> web_contents_states =
AllTabsWebContentsStateByteBuffer(env, jbyte_buffers,
std::move(saved_state_versions));
CreateHistoricalGroup(TabModelList::FindNativeTabModelForJavaObject(
ScopedJavaLocalRef<jobject>(env, jtab_model.obj())),
tab_group_id, saved_tab_group_id, title, (int)jcolor,
std::move(tabs_android),
std::move(web_contents_states));
}
static void JNI_HistoricalTabSaverImpl_CreateHistoricalBulkClosure(
JNIEnv* env,
const JavaParamRef<jobject>& jtab_model,
std::vector<int32_t>& root_ids,
const JavaParamRef<jobjectArray>& jtab_group_ids,
std::vector<std::u16string>& serialized_saved_tab_group_ids,
std::vector<std::u16string>& group_titles,
std::vector<int32_t>& group_colors,
std::vector<int32_t>& per_tab_root_id,
const JavaParamRef<jobjectArray>& jtabs_android,
const JavaParamRef<jobjectArray>& jbyte_buffers,
std::vector<int32_t>& saved_state_versions) {
std::vector<std::optional<tab_groups::TabGroupId>> tab_group_ids =
JavaTokensToTabGroupIds(env, jtab_group_ids);
std::vector<std::optional<base::Uuid>> saved_tab_group_ids =
StringsToUuids(serialized_saved_tab_group_ids);
int tabs_android_count = env->GetArrayLength(jtabs_android);
DCHECK_EQ(tabs_android_count, env->GetArrayLength(jbyte_buffers));
DCHECK_EQ(tabs_android_count, static_cast<int>(saved_state_versions.size()));
std::vector<WebContentsStateByteBuffer> web_contents_states =
AllTabsWebContentsStateByteBuffer(env, jbyte_buffers,
std::move(saved_state_versions));
CreateHistoricalBulkClosure(
TabModelList::FindNativeTabModelForJavaObject(
ScopedJavaLocalRef<jobject>(env, jtab_model.obj())),
std::move(root_ids), std::move(tab_group_ids),
std::move(saved_tab_group_ids), std::move(group_titles),
std::move(group_colors), std::move(per_tab_root_id),
TabAndroid::GetAllNativeTabs(
env, base::android::ScopedJavaLocalRef<jobjectArray>(jtabs_android)),
std::move(web_contents_states));
}
} // namespace historical_tab_saver