// Copyright 2013 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/recently_closed_tabs_bridge.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/android/token_android.h"
#include "base/containers/span.h"
#include "base/numerics/safe_conversions.h"
#include "base/token.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/flags/android/chrome_feature_list.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_restore.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/ui/android/tab_model/android_live_tab_context.h"
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#include "components/sessions/core/live_tab.h"
#include "components/sessions/core/tab_restore_service.h"
#include "content/public/browser/web_contents.h"
#include "url/android/gurl_android.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/android/chrome_jni_headers/RecentlyClosedBridge_jni.h"
#include "chrome/android/chrome_jni_headers/RecentlyClosedTab_jni.h"
using base::android::AttachCurrentThread;
using base::android::JavaParamRef;
using base::android::JavaRef;
using base::android::ScopedJavaGlobalRef;
using base::android::ScopedJavaLocalRef;
namespace recent_tabs {
namespace {
bool TabEntryWithIdExists(const sessions::TabRestoreService::Entries& entries,
SessionID session_id) {
const auto end = TabIterator::end(entries);
for (auto it = TabIterator::begin(entries); it != end; ++it) {
if (it->id == session_id) {
return true;
}
}
return false;
}
jni_zero::ScopedJavaLocalRef<jobject> CreateJavaRecentlyClosedTab(
JNIEnv* env,
const sessions::tab_restore::Tab& tab) {
const sessions::SerializedNavigationEntry& current_navigation =
tab.navigations.at(tab.current_navigation_index);
return Java_RecentlyClosedTab_Constructor(
env, tab.id.id(), tab.timestamp.InMillisecondsSinceUnixEpoch(),
current_navigation.title(), current_navigation.virtual_url(),
tab.group ? std::optional<base::Token>(tab.group->token())
: std::nullopt);
}
std::vector<jni_zero::ScopedJavaLocalRef<jobject>> PrepareTabs(
JNIEnv* env,
TabIterator& it,
const sessions::TabRestoreService::Entries::const_iterator& current_entry,
size_t tab_count) {
std::vector<jni_zero::ScopedJavaLocalRef<jobject>> ret;
ret.reserve(tab_count);
while (it.CurrentEntry() == current_entry) {
ret.push_back(CreateJavaRecentlyClosedTab(env, *it));
++it;
}
return ret;
}
// Add a tab entry to the main entries list.
void AddTabToEntries(JNIEnv* env,
const sessions::tab_restore::Tab& tab,
const JavaRef<jobject>& jentries) {
Java_RecentlyClosedBridge_addTabToEntries(
env, jentries, CreateJavaRecentlyClosedTab(env, tab));
}
void AddGroupToEntries(
JNIEnv* env,
TabIterator& it,
const sessions::TabRestoreService::Entries::const_iterator& current_entry,
const sessions::tab_restore::Group& group,
const JavaRef<jobject>& jentries) {
std::vector<jni_zero::ScopedJavaLocalRef<jobject>> tabs =
PrepareTabs(env, it, current_entry, group.tabs.size());
Java_RecentlyClosedBridge_addGroupToEntries(
env, jentries, group.id.id(),
group.timestamp.InMillisecondsSinceUnixEpoch(), group.visual_data.title(),
tabs);
}
void AddBulkEventToEntries(
JNIEnv* env,
TabIterator& it,
const sessions::TabRestoreService::Entries::const_iterator& current_entry,
const sessions::tab_restore::Window& window,
const JavaRef<jobject>& jentries) {
std::vector<jni_zero::ScopedJavaLocalRef<jobject>> tabs =
PrepareTabs(env, it, current_entry, window.tabs.size());
std::vector<std::optional<base::Token>> group_ids;
std::vector<const std::u16string*> group_titles;
const size_t group_count = window.tab_groups.size();
group_ids.reserve(group_count);
group_titles.reserve(group_count);
for (const auto& tab_group : window.tab_groups) {
group_ids.push_back(tab_group.first.token());
group_titles.push_back(&tab_group.second->visual_data.title());
}
Java_RecentlyClosedBridge_addBulkEventToEntries(
env, jentries, window.id.id(),
window.timestamp.InMillisecondsSinceUnixEpoch(), group_ids, group_titles,
tabs);
}
// Add `entries` to `jentries`.
void AddEntriesToList(JNIEnv* env,
const sessions::TabRestoreService::Entries& entries,
const JavaRef<jobject>& jentries,
int max_entry_count) {
int added_count = 0;
for (auto it = TabIterator::begin(entries);
it != TabIterator::end(entries) && added_count < max_entry_count;
++added_count) {
if (it.IsCurrentEntryTab()) {
AddTabToEntries(env, *it, jentries);
++it;
continue;
}
auto entry = it.CurrentEntry();
if ((*entry)->type == sessions::tab_restore::Type::GROUP) {
const auto& group =
static_cast<const sessions::tab_restore::Group&>(**entry);
AddGroupToEntries(env, it, entry, group, jentries);
continue;
}
if ((*entry)->type == sessions::tab_restore::Type::WINDOW) {
const auto& window =
static_cast<const sessions::tab_restore::Window&>(**entry);
AddBulkEventToEntries(env, it, entry, window, jentries);
continue;
}
NOTREACHED_IN_MIGRATION();
}
}
} // namespace
TabIterator::TabIterator(
const sessions::TabRestoreService::Entries& entries,
sessions::TabRestoreService::Entries::const_iterator it)
: entries_(entries), current_entry_(it) {
SetupInnerTabList();
}
TabIterator::~TabIterator() = default;
// static.
TabIterator TabIterator::begin(
const sessions::TabRestoreService::Entries& entries) {
return TabIterator(entries, entries.cbegin());
}
// static.
TabIterator TabIterator::end(
const sessions::TabRestoreService::Entries& entries) {
return TabIterator(entries, entries.cend());
}
bool TabIterator::IsCurrentEntryTab() const {
return (*current_entry_)->type == sessions::tab_restore::Type::TAB;
}
sessions::TabRestoreService::Entries::const_iterator TabIterator::CurrentEntry()
const {
return current_entry_;
}
TabIterator& TabIterator::operator++() {
// Early out at end.
if (current_entry_ == entries_->cend()) {
return *this;
}
// Iterate backward over current set of tabs if possible.
if (current_tab_ && tabs_ && current_tab_ != tabs_->crend()) {
(*current_tab_)++;
if (*current_tab_ != tabs_->crend()) {
return *this;
}
}
// At the end of an entry then go to the next entry.
tabs_ = nullptr;
current_tab_ = std::nullopt;
current_entry_++;
if (current_entry_ == entries_->cend()) {
return *this;
}
SetupInnerTabList();
return *this;
}
TabIterator TabIterator::operator++(int) {
TabIterator retval = *this;
++(*this);
return retval;
}
bool TabIterator::operator==(TabIterator other) const {
return current_entry_ == other.current_entry_ &&
current_tab_ == other.current_tab_;
}
bool TabIterator::operator!=(TabIterator other) const {
return !(*this == other);
}
const sessions::tab_restore::Tab& TabIterator::operator*() const {
return current_tab_
? ***current_tab_
: static_cast<const sessions::tab_restore::Tab&>(**current_entry_);
}
const sessions::tab_restore::Tab* TabIterator::operator->() const {
return current_tab_ ? (*current_tab_)->get()
: static_cast<const sessions::tab_restore::Tab*>(
current_entry_->get());
}
void TabIterator::SetupInnerTabList() {
if (current_entry_ == entries_->cend()) {
return;
}
if ((*current_entry_)->type == sessions::tab_restore::Type::GROUP) {
tabs_ =
&static_cast<const sessions::tab_restore::Group*>(current_entry_->get())
->tabs;
}
if ((*current_entry_)->type == sessions::tab_restore::Type::WINDOW) {
tabs_ = &static_cast<const sessions::tab_restore::Window*>(
current_entry_->get())
->tabs;
}
if (tabs_) {
current_tab_ = tabs_->crbegin();
if (current_tab_ == tabs_->crend()) {
++(*this);
}
}
}
RecentlyClosedTabsBridge::RecentlyClosedTabsBridge(
ScopedJavaGlobalRef<jobject> jbridge,
Profile* profile)
: bridge_(std::move(jbridge)),
profile_(profile),
tab_restore_service_(nullptr) {}
RecentlyClosedTabsBridge::~RecentlyClosedTabsBridge() {
if (tab_restore_service_) {
tab_restore_service_->RemoveObserver(this);
}
}
void RecentlyClosedTabsBridge::Destroy(JNIEnv* env) {
delete this;
}
jboolean RecentlyClosedTabsBridge::GetRecentlyClosedEntries(
JNIEnv* env,
const JavaParamRef<jobject>& jentries_list,
jint max_entry_count) {
EnsureTabRestoreService();
if (!tab_restore_service_) {
return false;
}
AddEntriesToList(env, tab_restore_service_->entries(), jentries_list,
max_entry_count);
return true;
}
jboolean RecentlyClosedTabsBridge::OpenRecentlyClosedTab(
JNIEnv* env,
const JavaParamRef<jobject>& jtab_model,
jint tab_session_id,
jint j_disposition) {
if (!tab_restore_service_) {
return false;
}
SessionID entry_id = SessionID::FromSerializedValue(tab_session_id);
// Ensure the corresponding tab entry from TabRestoreService is a tab.
if (!TabEntryWithIdExists(tab_restore_service_->entries(), entry_id)) {
return false;
}
auto* model = TabModelList::FindNativeTabModelForJavaObject(
ScopedJavaLocalRef<jobject>(env, jtab_model.obj()));
if (model == nullptr) {
return false;
}
AndroidLiveTabContextRestoreWrapper restore_context(model);
std::vector<sessions::LiveTab*> restored_tabs =
tab_restore_service_->RestoreEntryById(
&restore_context, entry_id,
static_cast<WindowOpenDisposition>(j_disposition));
return !restored_tabs.empty();
}
jboolean RecentlyClosedTabsBridge::OpenRecentlyClosedEntry(
JNIEnv* env,
const JavaParamRef<jobject>& jtab_model,
jint entry_session_id) {
// This should only be called when in bulk restore mode otherwise per-tab
// restore should always be used.
if (!tab_restore_service_) {
return false;
}
auto* model = TabModelList::FindNativeTabModelForJavaObject(
ScopedJavaLocalRef<jobject>(env, jtab_model.obj()));
if (model == nullptr) {
return false;
}
AndroidLiveTabContextRestoreWrapper restore_context(model);
std::vector<sessions::LiveTab*> restored_tabs =
tab_restore_service_->RestoreEntryById(
&restore_context, SessionID::FromSerializedValue(entry_session_id),
WindowOpenDisposition::NEW_BACKGROUND_TAB);
RestoreAndroidTabGroups(env, jtab_model, restore_context.GetTabGroups());
return !restored_tabs.empty();
}
jboolean RecentlyClosedTabsBridge::OpenMostRecentlyClosedEntry(
JNIEnv* env,
const JavaParamRef<jobject>& jtab_model) {
EnsureTabRestoreService();
if (!tab_restore_service_ || tab_restore_service_->entries().empty()) {
return false;
}
auto* model = TabModelList::FindNativeTabModelForJavaObject(
ScopedJavaLocalRef<jobject>(env, jtab_model.obj()));
if (model == nullptr) {
return false;
}
AndroidLiveTabContextRestoreWrapper restore_context(model);
std::vector<sessions::LiveTab*> restored_tabs;
// Do not use OpenMostRecentEntry as it uses WindowOpenDisposition::UNKNOWN.
// WindowOpenDisposition::UNKNOWN looks for a desktop window to use (N/A on
// Android) this ends up replacing `restore_context` with the base
// AndroidLiveTabContext. `restore_context` is required to rebuild groups
// information. To avoid this just use the first entry in entries when
// restoring.
restored_tabs = tab_restore_service_->RestoreEntryById(
&restore_context, tab_restore_service_->entries().front()->id,
WindowOpenDisposition::NEW_BACKGROUND_TAB);
RestoreAndroidTabGroups(env, jtab_model, restore_context.GetTabGroups());
return !restored_tabs.empty();
}
void RecentlyClosedTabsBridge::ClearRecentlyClosedEntries(JNIEnv* env) {
EnsureTabRestoreService();
if (tab_restore_service_) {
tab_restore_service_->ClearEntries();
}
}
void RecentlyClosedTabsBridge::TabRestoreServiceChanged(
sessions::TabRestoreService* service) {
Java_RecentlyClosedBridge_onUpdated(AttachCurrentThread(), bridge_);
}
void RecentlyClosedTabsBridge::TabRestoreServiceDestroyed(
sessions::TabRestoreService* service) {
tab_restore_service_ = nullptr;
}
void RecentlyClosedTabsBridge::EnsureTabRestoreService() {
if (tab_restore_service_) {
return;
}
tab_restore_service_ = TabRestoreServiceFactory::GetForProfile(profile_);
// TabRestoreServiceFactory::GetForProfile() can return nullptr (e.g. in
// incognito mode).
if (tab_restore_service_) {
// This does nothing if the tabs have already been loaded or they
// shouldn't be loaded.
tab_restore_service_->LoadTabsFromLastSession();
tab_restore_service_->AddObserver(this);
}
}
void RecentlyClosedTabsBridge::RestoreAndroidTabGroups(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& jtab_model,
const std::map<tab_groups::TabGroupId,
AndroidLiveTabContextRestoreWrapper::TabGroup>& groups) {
for (const auto& group : groups) {
std::string saved_tab_group_id =
group.second.saved_tab_group_id
? group.second.saved_tab_group_id->AsLowercaseString()
: "";
Java_RecentlyClosedBridge_restoreTabGroup(
env, bridge_, jtab_model, saved_tab_group_id,
group.second.visual_data.title(), (int)group.second.visual_data.color(),
group.second.tab_ids);
}
}
static jlong JNI_RecentlyClosedBridge_Init(JNIEnv* env,
const JavaParamRef<jobject>& jbridge,
Profile* profile) {
RecentlyClosedTabsBridge* bridge = new RecentlyClosedTabsBridge(
ScopedJavaGlobalRef<jobject>(env, jbridge.obj()), profile);
return reinterpret_cast<intptr_t>(bridge);
}
} // namespace recent_tabs