blob: c50bbbba64f57a5d5f52e23a482c893bee0b9a08 [file]
// Copyright 2026 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/sync/cross_device_theme_tracker_android.h"
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "chrome/browser/ntp_customization/jni_headers/CrossDeviceThemeTracker_jni.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sync/cross_device_theme_tracker_factory.h"
#include "third_party/jni_zero/default_conversions.h"
#include "third_party/jni_zero/system_jni/List_jni.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/color/color_provider_key.h"
#include "ui/color/dynamic_color/palette.h"
#include "ui/color/dynamic_color/palette_factory.h"
using base::android::AttachCurrentThread;
namespace themes {
CrossDeviceThemeTrackerAndroid::CrossDeviceThemeTrackerAndroid(
syncer::DeviceInfoTracker* device_info_tracker)
: CrossDeviceThemeTracker<sync_pb::ThemeAndroidSpecifics>(
device_info_tracker) {
JNIEnv* env = AttachCurrentThread();
// C++ creates and owns the Java counterpart.
java_object_.Reset(CrossDeviceThemeTrackerJni::create(
env, reinterpret_cast<intptr_t>(this)));
AddObserver(this);
}
CrossDeviceThemeTrackerAndroid::~CrossDeviceThemeTrackerAndroid() {
JNIEnv* env = AttachCurrentThread();
// Clear the native pointer on Java side to prevent calls into
// destroyed C++ object.
java_object_->clearNativePtr(env);
}
void CrossDeviceThemeTrackerAndroid::OnCrossDeviceThemeChanged() {
cached_java_themes_.Reset();
JNIEnv* env = AttachCurrentThread();
java_object_->notifyThemesChanged(env);
}
void CrossDeviceThemeTrackerAndroid::OnServiceStatusChanged(
ServiceStatus status) {
JNIEnv* env = AttachCurrentThread();
java_object_->notifyStatusChanged(env, status);
}
jni_zero::ScopedJavaLocalRef<JList<JNtpBackgroundDataBase>>
CrossDeviceThemeTrackerAndroid::GetThemes(
JNIEnv* env,
const jni_zero::JavaRef<JContext>& jcontext) {
if (!cached_java_themes_) {
RecreateJavaThemes(env, jcontext);
}
return cached_java_themes_.AsLocalRef(env);
}
ServiceStatus CrossDeviceThemeTrackerAndroid::GetServiceStatus(JNIEnv* env) {
return CrossDeviceThemeTracker<
sync_pb::ThemeAndroidSpecifics>::GetServiceStatus();
}
jni_zero::ScopedJavaLocalRef<jobject>
CrossDeviceThemeTrackerAndroid::CreateJavaTheme(
JNIEnv* env,
const jni_zero::JavaRef<JContext>& jcontext,
const DeviceThemeInfo<sync_pb::ThemeAndroidSpecifics>& theme_info) {
int32_t platform_type = static_cast<int32_t>(
MapToPlatformType(theme_info.os_type, theme_info.form_factor));
const sync_pb::ThemeAndroidSpecifics& specifics = theme_info.theme;
// 1. Extract background data if present.
bool has_background = specifics.has_ntp_background();
std::string bg_url;
std::string bg_collection_id;
bool is_bg_daily_refresh = false;
if (has_background) {
const sync_pb::NtpCustomBackground& bg = specifics.ntp_background();
bg_url = bg.url();
bg_collection_id = bg.collection_id();
is_bg_daily_refresh = bg.has_refresh_timestamp_unix_epoch_seconds();
}
// 2. Extract color data if present.
bool has_chrome_color = specifics.has_chrome_color_info();
int32_t chrome_color_id = 0;
bool is_color_daily_refresh = false;
bool has_user_color = false;
SkColor primary_light = 0;
SkColor primary_dark = 0;
SkColor background_light = 0;
SkColor background_dark = 0;
if (has_chrome_color) {
const sync_pb::ChromeColorInfo& color_info = specifics.chrome_color_info();
chrome_color_id = color_info.theme_color_id();
is_color_daily_refresh =
color_info.has_last_daily_update_timestamp_unix_epoch_millis();
} else if (specifics.has_user_color_theme() &&
specifics.user_color_theme().has_color()) {
has_user_color = true;
// TODO(crbug.com/517615321): Refactor this palette generation logic to a
// shared helper function in chrome/browser/themes/theme_service_utils.h
// so it can be shared with other theme consumers.
const sync_pb::UserColorTheme& user_color = specifics.user_color_theme();
SkColor seed_color = static_cast<SkColor>(user_color.color());
ui::ColorProviderKey::SchemeVariant scheme_variant =
ui::ColorProviderKey::SchemeVariant::kTonalSpot;
bool is_baseline = false;
if (user_color.has_browser_color_variant()) {
switch (user_color.browser_color_variant()) {
case sync_pb::UserColorTheme_BrowserColorVariant_SYSTEM:
case sync_pb::
UserColorTheme_BrowserColorVariant_BROWSER_COLOR_VARIANT_UNSPECIFIED:
is_baseline = true;
break;
case sync_pb::UserColorTheme_BrowserColorVariant_NEUTRAL:
scheme_variant = ui::ColorProviderKey::SchemeVariant::kNeutral;
break;
case sync_pb::UserColorTheme_BrowserColorVariant_VIBRANT:
scheme_variant = ui::ColorProviderKey::SchemeVariant::kVibrant;
break;
case sync_pb::UserColorTheme_BrowserColorVariant_EXPRESSIVE:
scheme_variant = ui::ColorProviderKey::SchemeVariant::kExpressive;
break;
case sync_pb::UserColorTheme_BrowserColorVariant_TONAL_SPOT:
default:
scheme_variant = ui::ColorProviderKey::SchemeVariant::kTonalSpot;
break;
}
} else {
is_baseline = true;
}
std::unique_ptr<ui::Palette> palette =
ui::GeneratePalette(seed_color, scheme_variant);
primary_light = palette->primary().get(40);
primary_dark = palette->primary().get(80);
background_light =
is_baseline ? palette->neutral().get(100) : palette->neutral().get(98);
background_dark =
is_baseline ? palette->neutral().get(25) : palette->secondary().get(25);
}
// 3. Send extracted info up to Java.
if (has_background) {
return CrossDeviceThemeTrackerJni::createThemeCollectionData(
env, jcontext, platform_type, bg_url, bg_collection_id,
is_bg_daily_refresh, has_chrome_color, chrome_color_id, has_user_color,
static_cast<int32_t>(primary_light));
} else if (has_chrome_color) {
return CrossDeviceThemeTrackerJni::createColorData(
env, jcontext, platform_type, chrome_color_id, is_color_daily_refresh);
} else if (has_user_color) {
return CrossDeviceThemeTrackerJni::createCustomizedColorData(
env, jcontext, platform_type, static_cast<int32_t>(primary_light),
static_cast<int32_t>(primary_dark),
static_cast<int32_t>(background_light),
static_cast<int32_t>(background_dark));
}
return nullptr;
}
jni_zero::ScopedJavaLocalRef<jobject>
CrossDeviceThemeTrackerAndroid::GetThemeForDeviceGuid(
JNIEnv* env,
const jni_zero::JavaRef<JContext>& jcontext,
const std::string& device_guid) {
auto other_themes = GetOtherDevicesThemes();
if (other_themes.empty()) {
return nullptr;
}
// 1. If a specific device_guid is provided, only return the theme matching
// that GUID. If that device does not have a theme (e.g. Themes sync was off
// on that device or default theme was used), return nullptr rather than
// falling back to another device's theme, to avoid combining preferences from
// one device and a theme from another.
if (!device_guid.empty()) {
for (const auto& theme_info : other_themes) {
if (theme_info.guid == device_guid) {
return CreateJavaTheme(env, jcontext, theme_info);
}
}
return nullptr;
}
// 2. If no device_guid was specified ("") (i.e. no preferences are being
// imported from any device), select the best candidate theme across all
// available devices. Prefer Android (same platform) first: when Themes sync
// is enabled, Android devices continuously auto-sync their themes, so
// aligning with the existing Android theme allows the importer to recognize
// that local and remote themes are already identical, avoiding redundant
// snackbar prompts and minimizing churn in synced theme colors.
const auto* best_theme = &other_themes.front();
for (const auto& theme_info : other_themes) {
if (theme_info.os_type == syncer::DeviceInfo::OsType::kAndroid) {
best_theme = &theme_info;
break;
}
}
return CreateJavaTheme(env, jcontext, *best_theme);
}
void CrossDeviceThemeTrackerAndroid::RecreateJavaThemes(
JNIEnv* env,
const jni_zero::JavaRef<JContext>& jcontext) {
auto other_themes = GetOtherDevicesThemes();
std::vector<jni_zero::ScopedJavaLocalRef<jobject>> java_themes_vector;
for (const auto& theme_info : other_themes) {
auto java_theme = CreateJavaTheme(env, jcontext, theme_info);
if (java_theme) {
java_themes_vector.emplace_back(std::move(java_theme));
}
}
auto java_themes = jni_zero::ToJniList(env, java_themes_vector)
.As<JList<JNtpBackgroundDataBase>>();
cached_java_themes_.Reset(java_themes);
}
PlatformType CrossDeviceThemeTrackerAndroid::MapToPlatformType(
syncer::DeviceInfo::OsType os_type,
syncer::DeviceInfo::FormFactor form_factor) {
switch (os_type) {
case syncer::DeviceInfo::OsType::kAndroid:
if (form_factor == syncer::DeviceInfo::FormFactor::kDesktop) {
return PlatformType::kDesktop;
}
return PlatformType::kAndroid;
case syncer::DeviceInfo::OsType::kIOS:
return PlatformType::kIos;
case syncer::DeviceInfo::OsType::kWindows:
case syncer::DeviceInfo::OsType::kMac:
case syncer::DeviceInfo::OsType::kLinux:
case syncer::DeviceInfo::OsType::kChromeOsAsh:
case syncer::DeviceInfo::OsType::kChromeOsLacros:
return PlatformType::kDesktop;
default:
return PlatformType::kUnknown;
}
}
} // namespace themes
static jni_zero::ScopedJavaLocalRef<JCrossDeviceThemeTracker>
JNI_CrossDeviceThemeTracker_GetForProfile(JNIEnv* env, Profile* profile) {
auto* tracker = CrossDeviceThemeTrackerFactory::GetForProfile(profile);
if (!tracker) {
return nullptr;
}
return static_cast<themes::CrossDeviceThemeTrackerAndroid*>(tracker)
->java_object()
.AsLocalRef(env);
}
DEFINE_JNI(CrossDeviceThemeTracker)