blob: 7a0a2c029a138f35068cbb22795e991e38357b10 [file] [log] [blame]
// 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/foreign_session_helper.h"
#include <jni.h>
#include <stddef.h>
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_restore.h"
#include "chrome/browser/sync/session_sync_service_factory.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/sync/service/sync_service.h"
#include "components/sync_sessions/open_tabs_ui_delegate.h"
#include "components/sync_sessions/session_sync_service.h"
#include "content/public/browser/web_contents.h"
#include "url/android/gurl_android.h"
// Must come after other includes, because FromJniType() uses Profile.
#include "chrome/browser/recent_tabs/jni_headers/ForeignSessionHelper_jni.h"
using base::android::AttachCurrentThread;
using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF16ToJavaString;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaParamRef;
using base::android::ScopedJavaGlobalRef;
using base::android::ScopedJavaLocalRef;
using sync_sessions::OpenTabsUIDelegate;
using sync_sessions::SyncedSession;
namespace {
OpenTabsUIDelegate* GetOpenTabsUIDelegate(Profile* profile) {
sync_sessions::SessionSyncService* service =
SessionSyncServiceFactory::GetInstance()->GetForProfile(profile);
// Only return the delegate if it exists.
if (!service) {
return nullptr;
}
return service->GetOpenTabsUIDelegate();
}
bool ShouldSkipTab(const sessions::SessionTab& session_tab) {
if (session_tab.navigations.empty()) {
return true;
}
int selected_index = session_tab.normalized_navigation_index();
const sessions::SerializedNavigationEntry& current_navigation =
session_tab.navigations.at(selected_index);
if (current_navigation.virtual_url().is_empty()) {
return true;
}
return false;
}
bool ShouldSkipWindow(const sessions::SessionWindow& window) {
for (const auto& tab_ptr : window.tabs) {
const sessions::SessionTab& session_tab = *(tab_ptr.get());
if (!ShouldSkipTab(session_tab)) {
return false;
}
}
return true;
}
bool ShouldSkipSession(const SyncedSession& session) {
for (const auto& window_pair : session.windows) {
const sessions::SessionWindow& window = window_pair.second->wrapped_window;
if (!ShouldSkipWindow(window)) {
return false;
}
}
return true;
}
void JNI_ForeignSessionHelper_CopyTabToJava(
JNIEnv* env,
const sessions::SessionTab& tab,
ScopedJavaLocalRef<jobject>& j_window) {
int selected_index = tab.normalized_navigation_index();
DCHECK_GE(selected_index, 0);
DCHECK_LT(selected_index, static_cast<int>(tab.navigations.size()));
const sessions::SerializedNavigationEntry& current_navigation =
tab.navigations.at(selected_index);
GURL tab_url = current_navigation.virtual_url();
Java_ForeignSessionHelper_pushTab(
env, j_window, url::GURLAndroid::FromNativeGURL(env, tab_url),
ConvertUTF16ToJavaString(env, current_navigation.title()),
tab.timestamp.InMillisecondsSinceUnixEpoch(),
tab.last_active_time.InMillisecondsSinceUnixEpoch(), tab.tab_id.id());
}
void JNI_ForeignSessionHelper_CopyWindowToJava(
JNIEnv* env,
const sessions::SessionWindow& window,
ScopedJavaLocalRef<jobject>& j_window) {
for (const auto& tab_ptr : window.tabs) {
const sessions::SessionTab& session_tab = *(tab_ptr.get());
if (ShouldSkipTab(session_tab)) {
return;
}
JNI_ForeignSessionHelper_CopyTabToJava(env, session_tab, j_window);
}
}
void JNI_ForeignSessionHelper_CopySessionToJava(
JNIEnv* env,
const SyncedSession& session,
ScopedJavaLocalRef<jobject>& j_session) {
for (const auto& window_pair : session.windows) {
const sessions::SessionWindow& window = window_pair.second->wrapped_window;
if (ShouldSkipWindow(window)) {
continue;
}
ScopedJavaLocalRef<jobject> last_pushed_window;
last_pushed_window.Reset(Java_ForeignSessionHelper_pushWindow(
env, j_session, window.timestamp.InMillisecondsSinceUnixEpoch(),
window.window_id.id()));
JNI_ForeignSessionHelper_CopyWindowToJava(env, window, last_pushed_window);
}
}
} // namespace
static jlong JNI_ForeignSessionHelper_Init(JNIEnv* env, Profile* profile) {
ForeignSessionHelper* foreign_session_helper =
new ForeignSessionHelper(profile);
return reinterpret_cast<intptr_t>(foreign_session_helper);
}
ForeignSessionHelper::ForeignSessionHelper(Profile* profile)
: profile_(profile) {
sync_sessions::SessionSyncService* service =
SessionSyncServiceFactory::GetInstance()->GetForProfile(profile);
// NOTE: The SessionSyncService can be null in tests.
if (service) {
// base::Unretained() is safe below because the subscription itself is a
// class member field and handles destruction well.
foreign_session_updated_subscription_ =
service->SubscribeToForeignSessionsChanged(base::BindRepeating(
&ForeignSessionHelper::FireForeignSessionCallback,
base::Unretained(this)));
}
}
ForeignSessionHelper::~ForeignSessionHelper() = default;
void ForeignSessionHelper::Destroy(JNIEnv* env) {
delete this;
}
jboolean ForeignSessionHelper::IsTabSyncEnabled(JNIEnv* env) {
sync_sessions::SessionSyncService* service =
SessionSyncServiceFactory::GetInstance()->GetForProfile(profile_);
return service && service->GetOpenTabsUIDelegate();
}
void ForeignSessionHelper::TriggerSessionSync(JNIEnv* env) {
syncer::SyncService* service = SyncServiceFactory::GetForProfile(profile_);
if (!service) {
return;
}
service->TriggerRefresh({syncer::SESSIONS});
}
void ForeignSessionHelper::SetOnForeignSessionCallback(
JNIEnv* env,
const JavaParamRef<jobject>& callback) {
callback_.Reset(env, callback);
}
void ForeignSessionHelper::FireForeignSessionCallback() {
if (callback_.is_null()) {
return;
}
JNIEnv* env = AttachCurrentThread();
Java_ForeignSessionCallback_onUpdated(env, callback_);
}
jboolean ForeignSessionHelper::GetForeignSessions(
JNIEnv* env,
const JavaParamRef<jobject>& result) {
OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(profile_);
if (!open_tabs) {
return false;
}
std::vector<raw_ptr<const SyncedSession, VectorExperimental>> sessions;
if (!open_tabs->GetAllForeignSessions(&sessions)) {
return false;
}
// Use a pref to keep track of sessions that were collapsed by the user.
// To prevent the pref from accumulating stale sessions, clear it each time
// and only add back sessions that are still current.
ScopedDictPrefUpdate pref_update(profile_->GetPrefs(),
prefs::kNtpCollapsedForeignSessions);
base::Value::Dict& pref_collapsed_sessions = pref_update.Get();
base::Value::Dict collapsed_sessions = pref_collapsed_sessions.Clone();
pref_collapsed_sessions.clear();
ScopedJavaLocalRef<jobject> last_pushed_session;
// Note: we don't own the SyncedSessions themselves.
for (size_t i = 0; i < sessions.size(); ++i) {
const SyncedSession& session = *(sessions[i]);
if (ShouldSkipSession(session)) {
continue;
}
const bool is_collapsed =
(collapsed_sessions.Find(session.GetSessionTag()) != nullptr);
if (is_collapsed) {
pref_collapsed_sessions.Set(session.GetSessionTag(), true);
}
last_pushed_session.Reset(Java_ForeignSessionHelper_pushSession(
env, result, ConvertUTF8ToJavaString(env, session.GetSessionTag()),
ConvertUTF8ToJavaString(env, session.GetSessionName()),
session.GetModifiedTime().InMillisecondsSinceUnixEpoch(),
static_cast<int>(session.GetDeviceFormFactor())));
// Push the full session, with tabs ordered by visual position.
JNI_ForeignSessionHelper_CopySessionToJava(env, session,
last_pushed_session);
}
return true;
}
jboolean ForeignSessionHelper::GetMobileAndTabletForeignSessions(
JNIEnv* env,
const JavaParamRef<jobject>& result) {
OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(profile_);
if (!open_tabs) {
return false;
}
std::vector<raw_ptr<const SyncedSession, VectorExperimental>> sessions;
if (!open_tabs->GetAllForeignSessions(&sessions)) {
return false;
}
ScopedJavaLocalRef<jobject> last_pushed_session;
size_t skipped_tabs_on_restore = 0;
// Note: we don't own the SyncedSessions themselves.
for (const SyncedSession* session : sessions) {
if (session->GetDeviceFormFactor() ==
syncer::DeviceInfo::FormFactor::kPhone ||
session->GetDeviceFormFactor() ==
syncer::DeviceInfo::FormFactor::kTablet) {
last_pushed_session.Reset(Java_ForeignSessionHelper_pushSession(
env, result, ConvertUTF8ToJavaString(env, session->GetSessionTag()),
ConvertUTF8ToJavaString(env, session->GetSessionName()),
session->GetModifiedTime().InMillisecondsSinceUnixEpoch(),
static_cast<int>(session->GetDeviceFormFactor())));
// Push the full session, with tabs ordered by visual position.
JNI_ForeignSessionHelper_CopySessionToJava(env, *session,
last_pushed_session);
} else {
skipped_tabs_on_restore++;
}
}
return (skipped_tabs_on_restore != sessions.size());
}
jboolean ForeignSessionHelper::OpenForeignSessionTab(
JNIEnv* env,
const JavaParamRef<jobject>& j_tab,
const JavaParamRef<jstring>& session_tag,
jint session_tab_id,
jint j_disposition) {
OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(profile_);
if (!open_tabs) {
LOG(ERROR) << "Null OpenTabsUIDelegate returned.";
return false;
}
const sessions::SessionTab* session_tab;
if (!open_tabs->GetForeignTab(ConvertJavaStringToUTF8(env, session_tag),
SessionID::FromSerializedValue(session_tab_id),
&session_tab)) {
LOG(ERROR) << "Failed to load foreign tab.";
return false;
}
if (session_tab->navigations.empty()) {
LOG(ERROR) << "Foreign tab no longer has valid navigations.";
return false;
}
TabAndroid* tab_android = TabAndroid::GetNativeTab(env, j_tab);
if (!tab_android)
return false;
content::WebContents* web_contents = tab_android->web_contents();
if (!web_contents) {
return false;
}
WindowOpenDisposition disposition =
static_cast<WindowOpenDisposition>(j_disposition);
SessionRestore::RestoreForeignSessionTab(web_contents, *session_tab,
disposition);
return true;
}
void ForeignSessionHelper::DeleteForeignSession(
JNIEnv* env,
const JavaParamRef<jstring>& session_tag) {
OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(profile_);
if (open_tabs) {
open_tabs->DeleteForeignSession(ConvertJavaStringToUTF8(env, session_tag));
}
}
void ForeignSessionHelper::SetInvalidationsForSessionsEnabled(
JNIEnv* env,
jboolean enabled) {
syncer::SyncService* service = SyncServiceFactory::GetForProfile(profile_);
if (!service) {
return;
}
service->SetInvalidationsForSessionsEnabled(enabled);
}
jint ForeignSessionHelper::OpenForeignSessionTabsAsBackgroundTabs(
JNIEnv* env,
const JavaParamRef<jobject>& j_tab,
const JavaParamRef<jintArray>& j_session_tab_ids,
const JavaParamRef<jstring>& session_tag) {
std::vector<int> session_tab_ids;
base::android::JavaIntArrayToIntVector(env, j_session_tab_ids,
&session_tab_ids);
int tabs_android_count = env->GetArrayLength(j_session_tab_ids);
TabAndroid* tab_android = TabAndroid::GetNativeTab(env, j_tab);
if (!tab_android) {
return 0;
}
// Open the first tab in the list with a renderer and web contents.
if (!ForeignSessionHelper::RestoreTabWithRenderer(session_tag, j_tab,
session_tab_ids[0])) {
return 0;
}
content::WebContents* web_contents = tab_android->web_contents();
if (!web_contents) {
return 0;
}
int num_tabs_restored = 1;
// Using the web contents of the first tab, load the rest of the tabs
// with no renderer and as background tabs.
for (int i = 1; i < tabs_android_count; i++) {
if (ForeignSessionHelper::RestoreTabNoRenderer(
session_tag, session_tab_ids[i], web_contents)) {
num_tabs_restored++;
}
}
return num_tabs_restored;
}
bool ForeignSessionHelper::RestoreTabWithRenderer(
const JavaParamRef<jstring>& session_tag,
const JavaParamRef<jobject>& j_tab,
int session_tab_id) {
JNIEnv* env = base::android::AttachCurrentThread();
OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(profile_);
if (!open_tabs) {
return false;
}
const sessions::SessionTab* foreground_session_tab;
if (!open_tabs->GetForeignTab(ConvertJavaStringToUTF8(env, session_tag),
SessionID::FromSerializedValue(session_tab_id),
&foreground_session_tab)) {
return false;
}
if (foreground_session_tab->navigations.empty()) {
return false;
}
TabAndroid* tab_android = TabAndroid::GetNativeTab(env, j_tab);
if (!tab_android) {
return false;
}
content::WebContents* web_contents = tab_android->web_contents();
if (!web_contents) {
return false;
}
SessionRestore::RestoreForeignSessionTab(web_contents,
*foreground_session_tab,
WindowOpenDisposition::CURRENT_TAB);
return true;
}
bool ForeignSessionHelper::RestoreTabNoRenderer(
const JavaParamRef<jstring>& session_tag,
int session_tab_id,
content::WebContents* web_contents) {
OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(profile_);
if (!open_tabs) {
return false;
}
const sessions::SessionTab* background_session_tab;
if (!open_tabs->GetForeignTab(
ConvertJavaStringToUTF8(base::android::AttachCurrentThread(),
session_tag),
SessionID::FromSerializedValue(session_tab_id),
&background_session_tab)) {
return false;
}
if (background_session_tab->navigations.empty()) {
return false;
}
SessionRestore::RestoreForeignSessionTab(
web_contents, *background_session_tab,
WindowOpenDisposition::NEW_BACKGROUND_TAB, true);
return true;
}